From 3ad0b8a0ea14ce2e18f78b2e9d632ade46c93d44 Mon Sep 17 00:00:00 2001 From: Jakob Linskeseder Date: Fri, 26 Sep 2025 22:04:38 +0200 Subject: [PATCH] fix: show modal when date related to a reminder is deleted According to the CalDAV specification, relative alarms without the date the alarm is related to are not allowed. In order to comply with the spec, we ask the user if the reminder should be converted to an absolute date or if it should be discarded. Fixes #2751 Signed-off-by: Jakob Linskeseder --- .../AppNavigation/ListItemCalendar.vue | 6 - src/components/AppSidebar/Alarm/AlarmList.vue | 120 ++++++++++++++---- .../AppSidebar/Alarm/AlarmListNew.vue | 18 ++- .../Alarm/AlarmRelationDeletionModal.vue | 85 +++++++++++++ src/models/alarm.js | 17 +++ src/models/task.js | 17 ++- src/store/tasks.js | 12 +- src/utils/alarms.js | 11 ++ src/views/AppSidebar.vue | 47 ++++--- 9 files changed, 269 insertions(+), 64 deletions(-) create mode 100644 src/components/AppSidebar/Alarm/AlarmRelationDeletionModal.vue diff --git a/src/components/AppNavigation/ListItemCalendar.vue b/src/components/AppNavigation/ListItemCalendar.vue index 36700f83e..e5116b95d 100644 --- a/src/components/AppNavigation/ListItemCalendar.vue +++ b/src/components/AppNavigation/ListItemCalendar.vue @@ -552,12 +552,6 @@ $color-error: #e9322d; display: none; position: relative; - &.error input.edit { - color: var(--color-error); - border-color: var(--color-error) !important; - box-shadow: 0 0 6px transparentize( $color-error, .7 ); - } - form { display: flex; diff --git a/src/components/AppSidebar/Alarm/AlarmList.vue b/src/components/AppSidebar/Alarm/AlarmList.vue index 029ca5ec7..ec3780479 100644 --- a/src/components/AppSidebar/Alarm/AlarmList.vue +++ b/src/components/AppSidebar/Alarm/AlarmList.vue @@ -9,7 +9,7 @@
-
+ + + diff --git a/src/models/alarm.js b/src/models/alarm.js index b10543976..2f932e6bc 100644 --- a/src/models/alarm.js +++ b/src/models/alarm.js @@ -8,6 +8,7 @@ import { getDateFromDateTimeValue, } from '../utils/alarms.js' import { AlarmComponent } from '@nextcloud/calendar-js' +import ICAL from 'ical.js' /** * Creates a complete alarm object based on given props @@ -97,6 +98,22 @@ const mapAlarmComponentToAlarmObject = (alarmComponent) => { } } +/** + * @param {Array} alarms ICAL.js alarms + */ +export function mapICALAlarmsToAlarmObjects(alarms) { + return alarms.map((alarm) => { + try { + return mapAlarmComponentToAlarmObject(AlarmComponent.fromICALJs(alarm)) + } catch (e) { + // Instead of breaking the whole page when parsing an invalid alarm, + // we just print a warning on the console. + console.warn(e) + return false + } + }).filter(Boolean) +} + /** * @param {number} time Total amount of seconds for the trigger * @param {boolean} relatedToStart If the alarm is related to the start of the event diff --git a/src/models/task.js b/src/models/task.js index dedbc0667..e3652b25b 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -626,18 +626,21 @@ export default class Task { /** * Remove an alarm * - * @param {number} index The index of the alarm-list + * @param {number[]} indexes The indexes of the alarm-list */ - removeAlarm(index) { + removeAlarm(indexes) { const valarms = this.vtodo.getAllSubcomponents('valarm') - const valarmToDelete = valarms[index] - if (valarmToDelete) { - this.vtodo.removeSubcomponent(valarms[index]) + for (const index of indexes) { + const valarmToDelete = valarms[index] - this.updateLastModified() - this._alarms = this.getAlarms() + if (valarmToDelete) { + this.vtodo.removeSubcomponent(valarmToDelete) + } } + + this.updateLastModified() + this._alarms = this.getAlarms() } /** diff --git a/src/store/tasks.js b/src/store/tasks.js index 362d0afff..4b33b7dc4 100644 --- a/src/store/tasks.js +++ b/src/store/tasks.js @@ -502,10 +502,10 @@ const mutations = { * @param {object} state The store data * @param {object} data Destructuring object * @param {Task} data.task The task - * @param {number} data.index The index of the alarm-item to remove + * @param {number[]} data.indexes The indexes of the alarm-items to remove */ - removeAlarm(state, { task, index }) { - task.removeAlarm(index) + removeAlarm(state, { task, indexes }) { + task.removeAlarm(indexes) }, /** @@ -1282,10 +1282,10 @@ const actions = { * Removes an alarm from a task * * @param {object} context The store context - * @param {Task} task The task to update + * @param {Task} task The task to remove */ - async removeAlarm(context, { task, index }) { - context.commit('removeAlarm', { task, index }) + async removeAlarm(context, { task, indexes }) { + context.commit('removeAlarm', { task, indexes }) context.dispatch('updateTask', task) }, diff --git a/src/utils/alarms.js b/src/utils/alarms.js index 88bbab968..6acbeda8f 100644 --- a/src/utils/alarms.js +++ b/src/utils/alarms.js @@ -258,3 +258,14 @@ export function getDateFromDateTimeValue(dateTimeValue) { 0, ) } + +/** + * Takes the related date and the relative trigger of an alarm and + * calculates the absolute date-time when the alarm will trigger. + * + * @param {Date} relatedDate Related date + * @param {number} relativeTrigger Relative trigger in seconds + */ +export function calculateAbsoluteDateFromRelativeTrigger(relatedDate, relativeTrigger) { + return new Date(((relatedDate.valueOf() / 1000) + relativeTrigger) * 1000) +} diff --git a/src/views/AppSidebar.vue b/src/views/AppSidebar.vue index 5e2e43019..ca93aff9e 100644 --- a/src/views/AppSidebar.vue +++ b/src/views/AppSidebar.vue @@ -234,14 +234,15 @@ License along with this library. If not, see . icon="TagMultiple" @add-tag="updateTag" @set-tags="updateTags" /> - + @remove-alarm="removeAlarmItem" + @restore-date="restoreDate"> @@ -282,7 +283,7 @@ import TagsItem from '../components/AppSidebar/TagsItem.vue' import TextItem from '../components/AppSidebar/TextItem.vue' import NotesItem from '../components/AppSidebar/NotesItem.vue' import TaskCheckbox from '../components/TaskCheckbox.vue' -// import TaskStatusDisplay from '../components/TaskStatusDisplay' +import { mapICALAlarmsToAlarmObjects } from '../models/alarm.js' import Task from '../models/task.js' import { startDateString, dueDateString } from '../utils/dateStrings.js' @@ -354,7 +355,6 @@ export default { CalendarPickerItem, NotesItem, TaskCheckbox, - // TaskStatusDisplay, }, /** * Before we navigate to a new task, we save possible edits to the task summary. @@ -410,6 +410,9 @@ export default { task() { return this.getTaskByRoute(this.$route) }, + alarms() { + return mapICALAlarmsToAlarmObjects(this.task.alarms) + }, summary() { return this.task ? this.task.summary : '' }, @@ -601,12 +604,6 @@ export default { return !!this.$store.state.settings.settings.allDay } }, - hasStartDate() { - return !!this.task.start - }, - hasDueDate() { - return !!this.task.due - }, showInCalendar() { // Only tasks with a due date show up in the calendar return !!this.showTaskInCalendar && this.task.dueMoment.isValid() @@ -916,12 +913,30 @@ export default { }, /** - * Removes an alarm + * Removes one or more alarms * - * @param {number} index The index of the alarm-item to remove + * @param {number[]} indexes The indexes of the alarm-items to remove */ - removeAlarmItem(index) { - this.removeAlarm({ task: this.task, index }) + removeAlarmItem(indexes) { + this.removeAlarm({ task: this.task, indexes }) + }, + + /** + * Restores the start or due-date + * + * This can happen if a start or due-date is deleted while there are still + * alarms relating to it. In case the user cancels the triggered modal, we + * have to restore the date. + * + * @param {Date} date The date previously deleted + * @param {boolean} isRelatedToStart True if the date is a start-date + */ + restoreDate(date, isRelatedToStart) { + if (isRelatedToStart) { + this.setStartDate({ task: this.task, value: date }) + } else { + this.setDueDate({ task: this.task, value: date }) + } }, async changeCalendar(calendar) {