Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions src/components/AppNavigation/ListItemCalendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
Comment thread
raimund-schluessler marked this conversation as resolved.

form {
display: flex;

Expand Down
120 changes: 95 additions & 25 deletions src/components/AppSidebar/Alarm/AlarmList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<slot name="icon" />
</div>
<div class="component__items">
<AlarmListItem v-for="(alarm, index) in alarmComponents"
<AlarmListItem v-for="(alarm, index) in alarms"
:key="index"
:index="index"
:alarm="alarm"
Expand All @@ -24,37 +24,44 @@
</p>
</div>
<AlarmListNew v-if="!readOnly"
:has-start-date="hasStartDate"
:has-due-date="hasDueDate"
:has-start-date="!!startDate"
:has-due-date="!!dueDate"
:is-all-day="allDay"
@add-alarm="addAlarm" />
</div>
</div>
<AlarmRelationDeletionModal v-if="alarmRelationDeletionModalIsOpen"
:alarms="relatedAlarms"
@keep="keepAlarms"
@discard="discardAlarms"
@close="restoreRelatedDate" />
</div>
</template>

<script>
import AlarmListNew from './AlarmListNew.vue'
import AlarmListItem from './AlarmListItem.vue'
import { mapAlarmComponentToAlarmObject } from '../../../models/alarm.js'

import { translate as t } from '@nextcloud/l10n'
import { AlarmComponent } from '@nextcloud/calendar-js'
import ICAL from 'ical.js'
import AlarmRelationDeletionModal from './AlarmRelationDeletionModal.vue'
import { calculateAbsoluteDateFromRelativeTrigger } from '../../../utils/alarms.js'
import { toRaw } from 'vue'

export default {
name: 'AlarmList',
components: {
AlarmRelationDeletionModal,
AlarmListItem,
AlarmListNew,
},
props: {
hasStartDate: {
type: Boolean,
startDate: {
type: [Date, null],
required: true,
},
hasDueDate: {
type: Boolean,
dueDate: {
type: [Date, null],
required: true,
},
readOnly: {
Expand All @@ -74,19 +81,22 @@ export default {
'addAlarm',
'removeAlarm',
'updateAlarm',
'restoreDate',
],
computed: {
alarmComponents() {
return this.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)
data() {
return {
alarmRelationDeletionModalIsOpen: false,
relatedAlarms: [],
relatedDate: null,
relatedDateIsRelatedToStart: null,
}
},
watch: {
startDate(newDate, oldDate) {
this.openModalIfAlarmsAreRelated(newDate, oldDate, true)
},
dueDate(newDueDate, oldDate) {
this.openModalIfAlarmsAreRelated(newDueDate, oldDate, false)
},
},
methods: {
Expand All @@ -104,7 +114,7 @@ export default {
action: 'DISPLAY',
// When the action is "DISPLAY", the alarm MUST also include a "DESCRIPTION" property
description: t('tasks', 'This is a todo reminder.'),
// The "REPEAT" property is only valid in combination with a "DURATION" propery
// The "REPEAT" property is only valid in combination with a "DURATION" property
repeat: 1,
duration: 'PT10M',
trigger: { value: undefined, parameter },
Expand Down Expand Up @@ -141,10 +151,68 @@ export default {
/**
* Removes an alarm from this event
*
* @param {number} index The index of the alarm-list
* @param {number|number[]} indexes The indexes of the alarm-list
*/
removeAlarm(indexes) {
this.$emit('removeAlarm', Array.isArray(indexes) ? indexes : [indexes])
},

/**
* Convert relative alarms to absolute alarms if related date is deleted
*/
keepAlarms() {
this.relatedAlarms.forEach((relatedAlarm) => {
const alarm = {
value: calculateAbsoluteDateFromRelativeTrigger(this.relatedDate, relatedAlarm.alarm.relativeTrigger),
parameter: undefined, // ical.js sets the correct parameter for us when using a `ICAL.Time`-object
}
this.updateAlarm(alarm, relatedAlarm.index)
})
this.completeRelationDeletionModal()
},

/**
* Discard all alarms if related date is deleted
*/
discardAlarms() {
this.removeAlarm(this.relatedAlarms.map((relatedAlarm) => relatedAlarm.index))
this.completeRelationDeletionModal()
},

/**
* Restore related date if modal was cancelled
*/
removeAlarm(index) {
this.$emit('removeAlarm', index)
restoreRelatedDate() {
this.$emit('restoreDate', this.relatedDate, this.relatedDateIsRelatedToStart)
this.completeRelationDeletionModal()
},

/**
* @param {Date|null} newDate The new date
* @param {ICAL.Time|null} oldDate The old date
* @param {boolean} isRelatedToStart If the date is related to the start
*/
openModalIfAlarmsAreRelated(newDate, oldDate, isRelatedToStart) {
const relatedAlarms = this.alarms.map((alarm, index) => {
if (alarm.isRelative && (alarm.relativeIsRelatedToStart === isRelatedToStart)) {
return { alarm, index }
}
return false
}).filter(Boolean)

if (newDate === null && relatedAlarms.length > 0) {
this.relatedAlarms = relatedAlarms
this.relatedDate = toRaw(oldDate).toJSDate()
this.relatedDateIsRelatedToStart = isRelatedToStart
this.alarmRelationDeletionModalIsOpen = true
}
},

completeRelationDeletionModal() {
this.alarmRelationDeletionModalIsOpen = false
this.relatedAlarms = []
this.relatedDate = null
this.relatedDateIsRelatedToStart = null
},
},
}
Expand Down Expand Up @@ -185,6 +253,8 @@ export default {

.new {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
justify-content: space-between;
width: 100%;
Expand Down
18 changes: 14 additions & 4 deletions src/components/AppSidebar/Alarm/AlarmListNew.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
-->

<template>
<NcActions type="tertiary"
<NcActions ref="actions"
type="tertiary"
:force-name="true"
:force-menu="true"
:menu-name="t('tasks', 'Add reminder')">
Expand Down Expand Up @@ -208,13 +209,12 @@ export default {
},

onBackToMenuClick() {
this.startDateMenuIsOpen = false
this.dueDateMenuIsOpen = false
this.chooseDateTimeMenuIsOpen = false
this.resetState()
},

onAlarmOptionClick(alarm) {
this.$emit('addAlarm', alarm)
this.resetState()
},

onChooseDateAndTime(date) {
Expand All @@ -224,6 +224,16 @@ export default {
}

this.$emit('addAlarm', alarm)
this.resetState()

// We have to close the menu manually because the `NcActions`-component isn't aware of what happens in
// the `AlarmDateTimePickerModal`-component and would stay open until one of its buttons are pressed.
this.$refs.actions.closeMenu()
},

resetState() {
this.startDateMenuIsOpen = false
this.dueDateMenuIsOpen = false
this.chooseDateTimeMenuIsOpen = false
},
},
Expand Down
85 changes: 85 additions & 0 deletions src/components/AppSidebar/Alarm/AlarmRelationDeletionModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<template>
<NcModal @close="onClose()">
<form class="content" @submit.prevent="onSubmit">
<h3 class="content__heading">
{{ t('tasks', 'Do you want to keep related reminders?') }}
</h3>
<p>
{{ n(
'tasks',
'This task has %n reminder. Would you like to keep it?',
'This task has %n reminders. Would you like to keep them?',
alarms.length
) }}
</p>
<div class="content__buttons">
<NcButton @click="onClose()">
{{ t('tasks', 'Cancel') }}
</NcButton>
<NcButton variant="warning" @click="onDiscard()">
{{ t('tasks', 'Discard reminders') }}
</NcButton>
<NcButton variant="primary" type="submit">
{{ t('tasks', 'Keep reminders') }}
</NcButton>
</div>
</form>
</NcModal>
</template>

<script>
import { translate as t } from '@nextcloud/l10n'
import { NcButton, NcModal } from '@nextcloud/vue'

export default {
name: 'AlarmRelationDeletionModal',
components: {
NcButton,
NcModal,
},
props: {
alarms: {
type: Array,
default: () => [],
},
},
emits: [
'close',
'discard',
'keep',
],
methods: {
t,
n,

onSubmit() {
this.$emit('keep')
},

onDiscard() {
this.$emit('discard')
},

onClose() {
this.$emit('close')
},
},
}
</script>

<style lang="scss" scoped>
.content {
padding: 14px;

&__heading {
margin-top: 0;
}

&__buttons {
display: flex;
gap: 8px;
margin-top: 28px;
justify-content: flex-end;
}
}
</style>
17 changes: 17 additions & 0 deletions src/models/alarm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -97,6 +98,22 @@ const mapAlarmComponentToAlarmObject = (alarmComponent) => {
}
}

/**
* @param {Array<ICAL.Component>} 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
Expand Down
17 changes: 10 additions & 7 deletions src/models/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

/**
Expand Down
Loading
Loading