Merge pull request #3001 from tchernobog/feature/default-reminder-setting

Allow user to set default reminder duration for new events
This commit is contained in:
Christoph Wurst 2021-05-07 17:24:53 +02:00 committed by GitHub
commit ead3fac8aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 270 additions and 5 deletions

View File

@ -55,9 +55,24 @@
}
&--slotDuration,
&--defaultReminder {
display: table;
label {
display: table-cell;
min-width: 50%;
vertical-align: middle;
}
.multiselect {
display: table-cell;
width: 50%;
}
}
&--timezone {
width: 100%;
.multiselect {
width: 100%;
}

View File

@ -120,6 +120,7 @@ class PublicViewController extends Controller {
$defaultSkipPopover = $this->config->getAppValue($this->appName, 'skipPopover', 'yes');
$defaultTimezone = $this->config->getAppValue($this->appName, 'timezone', 'automatic');
$defaultSlotDuration = $this->config->getAppValue($this->appName, 'slotDuration', '00:30:00');
$defaultDefaultReminder = $this->config->getAppValue($this->appName, 'defaultReminder', 'none');
$defaultShowTasks = $this->config->getAppValue($this->appName, 'showTasks', 'yes');
$appVersion = $this->config->getAppValue($this->appName, 'installed_version', null);
@ -134,6 +135,7 @@ class PublicViewController extends Controller {
$this->initialStateService->provideInitialState($this->appName, 'talk_enabled', false);
$this->initialStateService->provideInitialState($this->appName, 'timezone', $defaultTimezone);
$this->initialStateService->provideInitialState($this->appName, 'slot_duration', $defaultSlotDuration);
$this->initialStateService->provideInitialState($this->appName, 'default_reminder', $defaultDefaultReminder);
$this->initialStateService->provideInitialState($this->appName, 'show_tasks', $defaultShowTasks === 'yes');
$this->initialStateService->provideInitialState($this->appName, 'tasks_enabled', false);

View File

@ -87,6 +87,8 @@ class SettingsController extends Controller {
return $this->setEventLimit($value);
case 'slotDuration':
return $this->setSlotDuration($value);
case 'defaultReminder':
return $this->setDefaultReminder($value);
case 'showTasks':
return $this->setShowTasks($value);
default:
@ -310,4 +312,31 @@ class SettingsController extends Controller {
return new JSONResponse();
}
/**
* sets defaultReminder for user
*
* @param string $value User-selected option for default_reminder in agenda view
* @return JSONResponse
*/
private function setDefaultReminder(string $value):JSONResponse {
if ($value !== 'none' &&
filter_var($value, FILTER_VALIDATE_INT,
['options' => ['max_range' => 0]]) === false) {
return new JSONResponse([], Http::STATUS_UNPROCESSABLE_ENTITY);
}
try {
$this->config->setUserValue(
$this->userId,
$this->appName,
'defaultReminder',
$value
);
} catch (\Exception $e) {
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
}
return new JSONResponse();
}
}

View File

@ -94,6 +94,7 @@ class ViewController extends Controller {
$defaultSkipPopover = $this->config->getAppValue($this->appName, 'skipPopover', 'no');
$defaultTimezone = $this->config->getAppValue($this->appName, 'timezone', 'automatic');
$defaultSlotDuration = $this->config->getAppValue($this->appName, 'slotDuration', '00:30:00');
$defaultDefaultReminder = $this->config->getAppValue($this->appName, 'defaultReminder', 'none');
$defaultShowTasks = $this->config->getAppValue($this->appName, 'showTasks', 'yes');
$appVersion = $this->config->getAppValue($this->appName, 'installed_version', null);
@ -105,6 +106,7 @@ class ViewController extends Controller {
$skipPopover = $this->config->getUserValue($this->userId, $this->appName, 'skipPopover', $defaultSkipPopover) === 'yes';
$timezone = $this->config->getUserValue($this->userId, $this->appName, 'timezone', $defaultTimezone);
$slotDuration = $this->config->getUserValue($this->userId, $this->appName, 'slotDuration', $defaultSlotDuration);
$defaultReminder = $this->config->getUserValue($this->userId, $this->appName, 'defaultReminder', $defaultDefaultReminder);
$showTasks = $this->config->getUserValue($this->userId, $this->appName, 'showTasks', $defaultShowTasks) === 'yes';
$talkEnabled = $this->appManager->isEnabledForUser('spreed');
@ -120,6 +122,7 @@ class ViewController extends Controller {
$this->initialStateService->provideInitialState($this->appName, 'talk_enabled', $talkEnabled);
$this->initialStateService->provideInitialState($this->appName, 'timezone', $timezone);
$this->initialStateService->provideInitialState($this->appName, 'slot_duration', $slotDuration);
$this->initialStateService->provideInitialState($this->appName, 'default_reminder', $defaultReminder);
$this->initialStateService->provideInitialState($this->appName, 'show_tasks', $showTasks);
$this->initialStateService->provideInitialState($this->appName, 'tasks_enabled', $tasksEnabled);

View File

@ -66,7 +66,9 @@
{{ $t('calendar', 'Show week numbers') }}
</ActionCheckbox>
<li class="settings-fieldset-interior-item settings-fieldset-interior-item--slotDuration">
<label for="slotDuration">{{ $t('calendar', 'Time increments') }}</label>
<Multiselect
:id="slotDuration"
:allow-empty="false"
:options="slotDurationOptions"
:value="selectedDurationOption"
@ -75,6 +77,18 @@
label="label"
@select="changeSlotDuration" />
</li>
<li class="settings-fieldset-interior-item settings-fieldset-interior-item--defaultReminder">
<label for="defaultReminder">{{ $t('calendar', 'Default reminder') }}</label>
<Multiselect
:id="defaultReminder"
:allow-empty="false"
:options="defaultReminderOptions"
:value="selectedDefaultReminderOption"
:disabled="savingDefaultReminder"
track-by="value"
label="label"
@select="changeDefaultReminder" />
</li>
<SettingsTimezoneSelect :is-disabled="loadingCalendars" />
<ActionButton class="settings-fieldset-interior-item" icon="icon-clippy" @click.prevent.stop="copyPrimaryCalDAV">
{{ $t('calendar', 'Copy primary CalDAV address') }}
@ -124,6 +138,8 @@ import {
IMPORT_STAGE_PROCESSING,
} from '../../models/consts.js'
import { getDefaultAlarms } from '../../defaults/defaultAlarmProvider.js'
export default {
name: 'Settings',
components: {
@ -148,6 +164,7 @@ export default {
savingTasks: false,
savingPopover: false,
savingSlotDuration: false,
savingDefaultReminder: false,
savingWeekend: false,
savingWeekNumber: false,
displayKeyboardShortcuts: false,
@ -164,6 +181,7 @@ export default {
showWeekends: state => state.settings.showWeekends,
showWeekNumbers: state => state.settings.showWeekNumbers,
slotDuration: state => state.settings.slotDuration,
defaultReminder: state => state.settings.defaultReminder,
timezone: state => state.settings.timezone,
locale: (state) => state.settings.momentLocale,
}),
@ -209,6 +227,22 @@ export default {
selectedDurationOption() {
return this.slotDurationOptions.find(o => o.value === this.slotDuration)
},
defaultReminderOptions() {
const defaultAlarms = getDefaultAlarms().map(seconds => {
return {
label: moment.duration(Math.abs(seconds) * 1000).locale(this.locale).humanize(),
value: seconds.toString(),
}
})
return [{
label: this.$t('calendar', 'No reminder'),
value: 'none',
}].concat(defaultAlarms)
},
selectedDefaultReminderOption() {
return this.defaultReminderOptions.find(o => o.value === this.defaultReminder)
},
},
methods: {
async toggleBirthdayEnabled() {
@ -310,6 +344,30 @@ export default {
this.savingSlotDuration = false
}
},
/**
* Updates the setting for the default reminder
*
* @param {Object} option The new selected value
*/
async changeDefaultReminder(option) {
if (!option) {
return
}
// change to loading status
this.savingDefaultReminder = true
try {
await this.$store.dispatch('setDefaultReminder', {
defaultReminder: option.value,
})
this.savingDefaultReminder = false
} catch (error) {
console.error(error)
showError(this.$t('calendar', 'New setting was not saved successfully.'))
this.savingDefaultReminder = false
}
},
/**
* Copies the primary CalDAV url to the user's clipboard.
*/

View File

@ -45,6 +45,8 @@ import {
} from '../utils/color.js'
import { mapAlarmComponentToAlarmObject } from '../models/alarm.js'
import { getObjectAtRecurrenceId } from '../utils/calendarObject.js'
import logger from '../utils/logger.js'
import settings from './settings.js'
const state = {
isNew: null,
@ -1430,6 +1432,18 @@ const actions = {
const eventComponent = getObjectAtRecurrenceId(calendarObject, startDate)
const calendarObjectInstance = mapEventComponentToEventObject(eventComponent)
// Add an alarm if the user set a default one in the settings. If
// not, defaultReminder will not be a number (rather the string "none").
const defaultReminder = parseInt(settings.state.defaultReminder)
if (!isNaN(defaultReminder)) {
commit('addAlarmToCalendarObjectInstance', {
calendarObjectInstance: calendarObjectInstance,
type: 'DISPLAY',
totalSeconds: defaultReminder,
})
logger.debug(`Added defaultReminder (${defaultReminder}s) to newly created event`)
}
commit('setCalendarObjectInstanceForNewEvent', {
calendarObject,
calendarObjectInstance,

View File

@ -38,6 +38,7 @@ const state = {
showWeekNumbers: null,
skipPopover: null,
slotDuration: null,
defaultReminder: null,
tasksEnabled: false,
timezone: 'automatic',
// user-defined Nextcloud settings
@ -102,6 +103,17 @@ const mutations = {
state.slotDuration = slotDuration
},
/**
* Updates the user's preferred defaultReminder
*
* @param {Object} state The Vuex state
* @param {Object} data The destructuring object
* @param {String} data.defaultReminder The new default reminder length
*/
setDefaultReminder(state, { defaultReminder }) {
state.defaultReminder = defaultReminder
},
/**
* Updates the user's timezone
*
@ -126,11 +138,12 @@ const mutations = {
* @param {Boolean} data.showWeekends Whether or not to display weekends
* @param {Boolean} data.skipPopover Whether or not to skip the simple event popover
* @param {String} data.slotDuration The duration of one slot in the agendaView
* @param {String} data.defaultReminder The default reminder to set on newly created events
* @param {Boolean} data.talkEnabled Whether or not the talk app is enabled
* @param {Boolean} data.tasksEnabled Whether ot not the tasks app is enabled
* @param {String} data.timezone The timezone to view the calendar in. Either an Olsen timezone or "automatic"
*/
loadSettingsFromServer(state, { appVersion, eventLimit, firstRun, showWeekNumbers, showTasks, showWeekends, skipPopover, slotDuration, talkEnabled, tasksEnabled, timezone }) {
loadSettingsFromServer(state, { appVersion, eventLimit, firstRun, showWeekNumbers, showTasks, showWeekends, skipPopover, slotDuration, defaultReminder, talkEnabled, tasksEnabled, timezone }) {
logInfo(`
Initial settings:
- AppVersion: ${appVersion}
@ -141,6 +154,7 @@ Initial settings:
- ShowWeekends: ${showWeekends}
- SkipPopover: ${skipPopover}
- SlotDuration: ${slotDuration}
- DefaultReminder: ${defaultReminder}
- TalkEnabled: ${talkEnabled}
- TasksEnabled: ${tasksEnabled}
- Timezone: ${timezone}
@ -154,6 +168,7 @@ Initial settings:
state.showWeekends = showWeekends
state.skipPopover = skipPopover
state.slotDuration = slotDuration
state.defaultReminder = defaultReminder
state.talkEnabled = talkEnabled
state.tasksEnabled = tasksEnabled
state.timezone = timezone
@ -319,6 +334,24 @@ const actions = {
commit('setSlotDuration', { slotDuration })
},
/**
* Updates the user's preferred defaultReminder
*
* @param {Object} vuex The Vuex destructuring object
* @param {Object} vuex.state The Vuex state
* @param {Function} vuex.commit The Vuex commit Function
* @param {Object} data The destructuring object
* @param {String} data.defaultReminder The new default reminder
*/
async setDefaultReminder({ state, commit }, { defaultReminder }) {
if (state.defaultReminder === defaultReminder) {
return
}
await setConfig('defaultReminder', defaultReminder)
commit('setDefaultReminder', { defaultReminder })
},
/**
* Updates the user's timezone
*

View File

@ -124,6 +124,7 @@ export default {
showWeekends: state => state.settings.showWeekends,
showWeekNumbers: state => state.settings.showWeekNumbers,
slotDuration: state => state.settings.slotDuration,
defaultReminder: state => state.settings.defaultReminder,
showTasks: state => state.settings.showTasks,
timezone: state => state.settings.timezone,
modificationCount: state => state.calendarObjects.modificationCount,
@ -191,6 +192,7 @@ export default {
showWeekNumbers: loadState('calendar', 'show_week_numbers'),
skipPopover: loadState('calendar', 'skip_popover'),
slotDuration: loadState('calendar', 'slot_duration'),
defaultReminder: loadState('calendar', 'default_reminder'),
talkEnabled: loadState('calendar', 'talk_enabled'),
tasksEnabled: loadState('calendar', 'tasks_enabled'),
timezone: loadState('calendar', 'timezone'),

View File

@ -56,6 +56,7 @@ describe('store/settings test suite', () => {
showWeekNumbers: null,
skipPopover: null,
slotDuration: null,
defaultReminder: null,
tasksEnabled: false,
timezone: 'automatic',
momentLocale: 'en',
@ -131,6 +132,15 @@ describe('store/settings test suite', () => {
expect(state.slotDuration).toEqual('00:30:00')
})
it('should provide a mutation to set the default reminder duration setting', () => {
const state = {
defaultReminder: 'previousValue',
}
settingsStore.mutations.setDefaultReminder(state, { defaultReminder: '-300' })
expect(state.defaultReminder).toEqual('-300')
})
it('should provide a mutation to set the timezone setting', () => {
const state = {
timezone: 'previousValue',
@ -151,6 +161,7 @@ describe('store/settings test suite', () => {
showWeekNumbers: null,
skipPopover: null,
slotDuration: null,
defaultReminder: null,
tasksEnabled: false,
timezone: 'automatic',
momentLocale: 'en',
@ -166,6 +177,7 @@ describe('store/settings test suite', () => {
showWeekends: true,
skipPopover: true,
slotDuration: '00:30:00',
defaultReminder: '-600',
talkEnabled: false,
tasksEnabled: true,
timezone: 'Europe/Berlin',
@ -185,6 +197,7 @@ Initial settings:
- ShowWeekends: true
- SkipPopover: true
- SlotDuration: 00:30:00
- DefaultReminder: -600
- TalkEnabled: false
- TasksEnabled: true
- Timezone: Europe/Berlin
@ -198,6 +211,7 @@ Initial settings:
showWeekends: true,
skipPopover: true,
slotDuration: '00:30:00',
defaultReminder: '-600',
talkEnabled: false,
tasksEnabled: true,
timezone: 'Europe/Berlin',
@ -535,6 +549,38 @@ Initial settings:
expect(commit).toHaveBeenNthCalledWith(1, 'setSlotDuration', { slotDuration: '00:30:00' })
})
it('should provide an action to set the default reminder setting - same value', async () => {
expect.assertions(2)
const state = {
defaultReminder: 'none'
}
const commit = jest.fn()
await settingsStore.actions.setDefaultReminder({ state, commit }, { defaultReminder: 'none' })
expect(setConfig).toHaveBeenCalledTimes(0)
expect(commit).toHaveBeenCalledTimes(0)
})
it('should provide an action to set the default reminder setting - different value', async () => {
expect.assertions(4)
const state = {
defaultReminder: 'none'
}
const commit = jest.fn()
setConfig.mockResolvedValueOnce()
await settingsStore.actions.setDefaultReminder({ state, commit }, { defaultReminder: '00:10:00' })
expect(setConfig).toHaveBeenCalledTimes(1)
expect(setConfig).toHaveBeenNthCalledWith(1, 'defaultReminder', '00:10:00')
expect(commit).toHaveBeenCalledTimes(1)
expect(commit).toHaveBeenNthCalledWith(1, 'setDefaultReminder', { defaultReminder: '00:10:00' })
})
it('should provide an action to set the timezone setting - same value', async () => {
expect.assertions(2)

View File

@ -63,7 +63,7 @@ class PublicViewControllerTest extends TestCase {
}
public function testPublicIndexWithBranding():void {
$this->config->expects(self::exactly(9))
$this->config->expects(self::exactly(10))
->method('getAppValue')
->willReturnMap([
['calendar', 'eventLimit', 'yes', 'no'],
@ -73,6 +73,7 @@ class PublicViewControllerTest extends TestCase {
['calendar', 'skipPopover', 'yes', 'yes'],
['calendar', 'timezone', 'automatic', 'defaultTimezone'],
['calendar', 'slotDuration', '00:30:00', 'defaultSlotDuration'],
['calendar', 'defaultReminder', 'none', 'defaultDefaultReminder'],
['calendar', 'showTasks', 'yes', 'yes'],
['calendar', 'installed_version', null, '1.0.0']
]);
@ -99,7 +100,7 @@ class PublicViewControllerTest extends TestCase {
->with('imagePath456')
->willReturn('absoluteImagePath456');
$this->initialStateService->expects(self::exactly(12))
$this->initialStateService->expects(self::exactly(13))
->method('provideInitialState')
->withConsecutive(
['calendar', 'app_version', '1.0.0'],
@ -112,6 +113,7 @@ class PublicViewControllerTest extends TestCase {
['calendar', 'talk_enabled', false],
['calendar', 'timezone', 'defaultTimezone'],
['calendar', 'slot_duration', 'defaultSlotDuration'],
['calendar', 'default_reminder', 'defaultDefaultReminder'],
['calendar', 'show_tasks', true],
['calendar', 'tasks_enabled', false]
);
@ -138,6 +140,7 @@ class PublicViewControllerTest extends TestCase {
['calendar', 'skipPopover', 'yes', 'yes'],
['calendar', 'timezone', 'automatic', 'defaultTimezone'],
['calendar', 'slotDuration', '00:30:00', 'defaultSlotDuration'],
['calendar', 'defaultReminder', 'none', 'defaultDefaultReminder'],
['calendar', 'showTasks', 'yes', 'defaultShowTasks'],
['calendar', 'installed_version', null, '1.0.0']
]);
@ -163,7 +166,7 @@ class PublicViewControllerTest extends TestCase {
->with('imagePath456')
->willReturn('absoluteImagePath456');
$this->initialStateService->expects(self::exactly(12))
$this->initialStateService->expects(self::exactly(13))
->method('provideInitialState')
->withConsecutive(
['calendar', 'app_version', '1.0.0'],
@ -176,6 +179,7 @@ class PublicViewControllerTest extends TestCase {
['calendar', 'talk_enabled', false],
['calendar', 'timezone', 'defaultTimezone'],
['calendar', 'slot_duration', 'defaultSlotDuration'],
['calendar', 'default_reminder', 'defaultDefaultReminder'],
['calendar', 'show_tasks', false],
['calendar', 'tasks_enabled', false]
);

View File

@ -370,6 +370,59 @@ class SettingsControllerTest extends TestCase {
$this->assertEquals(500, $actual->getStatus());
}
/**
* @param string $value
* @param int $expectedStatusCode
*
* @dataProvider setDefaultReminderWithAllowedValueDataProvider
*/
public function testSetDefaultReminderWithAllowedValue(string $value,
int $expectedStatusCode):void {
if ($expectedStatusCode === 200) {
$this->config->expects($this->once())
->method('setUserValue')
->with('user123', $this->appName, 'defaultReminder', $value);
}
$actual = $this->controller->setConfig('defaultReminder', $value);
$this->assertInstanceOf('OCP\AppFramework\Http\JSONResponse', $actual);
$this->assertEquals([], $actual->getData());
$this->assertEquals($expectedStatusCode, $actual->getStatus());
}
public function setDefaultReminderWithAllowedValueDataProvider():array {
return [
['none', 200],
['-0', 200],
['0', 200],
['-300', 200],
['-600', 200],
['-900', 200],
['-1200', 200],
['-2400', 200],
['-2400', 200],
['not-none', 422],
['NaN', 422],
['0.1', 422],
['1', 422],
['300', 422],
];
}
public function testSetDefaultReminderWithException():void {
$this->config->expects($this->once())
->method('setUserValue')
->with('user123', $this->appName, 'defaultReminder', 'none')
->will($this->throwException(new \Exception));
$actual = $this->controller->setConfig('defaultReminder', 'none');
$this->assertInstanceOf('OCP\AppFramework\Http\JSONResponse', $actual);
$this->assertEquals([], $actual->getData());
$this->assertEquals(500, $actual->getStatus());
}
public function testSetNotExistingConfig():void {
$actual = $this->controller->setConfig('foo', 'bar');

View File

@ -78,6 +78,7 @@ class ViewControllerTest extends TestCase {
['calendar', 'skipPopover', 'no', 'defaultSkipPopover'],
['calendar', 'timezone', 'automatic', 'defaultTimezone'],
['calendar', 'slotDuration', '00:30:00', 'defaultSlotDuration'],
['calendar', 'defaultReminder', 'none', 'defaultDefaultReminder'],
['calendar', 'showTasks', 'yes', 'defaultShowTasks'],
['calendar', 'installed_version', null, '1.0.0'],
]);
@ -92,6 +93,7 @@ class ViewControllerTest extends TestCase {
['user123', 'calendar', 'skipPopover', 'defaultSkipPopover', 'yes'],
['user123', 'calendar', 'timezone', 'defaultTimezone', 'Europe/Berlin'],
['user123', 'calendar', 'slotDuration', 'defaultSlotDuration', '00:15:00'],
['user123', 'calendar', 'defaultReminder', 'defaultDefaultReminder', '00:10:00'],
['user123', 'calendar', 'showTasks', 'defaultShowTasks', '00:15:00'],
]);
$this->appManager
@ -114,6 +116,7 @@ class ViewControllerTest extends TestCase {
['calendar', 'talk_enabled', true],
['calendar', 'timezone', 'Europe/Berlin'],
['calendar', 'slot_duration', '00:15:00'],
['calendar', 'default_reminder', '00:10:00'],
['calendar', 'show_tasks', false],
['calendar', 'tasks_enabled', true]
);
@ -143,6 +146,7 @@ class ViewControllerTest extends TestCase {
['calendar', 'skipPopover', 'no', 'defaultSkipPopover'],
['calendar', 'timezone', 'automatic', 'defaultTimezone'],
['calendar', 'slotDuration', '00:30:00', 'defaultSlotDuration'],
['calendar', 'defaultReminder', 'none', 'defaultDefaultReminder'],
['calendar', 'showTasks', 'yes', 'defaultShowTasks'],
['calendar', 'installed_version', null, '1.0.0'],
]);
@ -157,6 +161,7 @@ class ViewControllerTest extends TestCase {
['user123', 'calendar', 'skipPopover', 'defaultSkipPopover', 'yes'],
['user123', 'calendar', 'timezone', 'defaultTimezone', 'Europe/Berlin'],
['user123', 'calendar', 'slotDuration', 'defaultSlotDuration', '00:15:00'],
['user123', 'calendar', 'defaultReminder', 'defaultDefaultReminder', '00:10:00'],
['user123', 'calendar', 'showTasks', 'defaultShowTasks', '00:15:00'],
]);
$this->appManager
@ -179,6 +184,7 @@ class ViewControllerTest extends TestCase {
['calendar', 'talk_enabled', true],
['calendar', 'timezone', 'Europe/Berlin'],
['calendar', 'slot_duration', '00:15:00'],
['calendar', 'default_reminder', '00:10:00'],
['calendar', 'show_tasks', false],
['calendar', 'tasks_enabled', false]
);