-
{{ item.name }}
+
{{ item.name }}
{{ item.subline }}
@@ -77,7 +78,7 @@
|
-
+ |
@@ -308,19 +309,22 @@ th {
color: var(--color-text-maxcontrast)
}
-.name {
- // Take remaining width to prevent whitespace on the right side
- width: 100vw;
-}
-
.item {
display: flex;
+ .item-name {
+ white-space: normal;
+ }
+
.item-subline {
color: var(--color-text-maxcontrast)
}
}
+.item-actions {
+ text-align: right;
+}
+
.deletedAt {
text-align: right;
}
diff --git a/src/components/AppNavigation/Settings.vue b/src/components/AppNavigation/Settings.vue
index 969744ead512170a7019b9df2a5872c2a1b2ab37..452289d8f7f4e71782d1988638e6d0b847b74d8d 100644
--- a/src/components/AppNavigation/Settings.vue
+++ b/src/components/AppNavigation/Settings.vue
@@ -218,7 +218,7 @@ export default {
return this.$store.state.importState.importState.stage === IMPORT_STAGE_IMPORTING
},
settingsTitle() {
- return this.$t('calendar', 'Settings & import').replace(/&/g, '&')
+ return this.$t('calendar', 'Calendar settings')
},
slotDurationOptions() {
return [{
diff --git a/src/components/AppNavigation/Settings/ShortcutOverview.vue b/src/components/AppNavigation/Settings/ShortcutOverview.vue
index 90274a25ba4d6932d440514c4f25fd8c6c61552b..ca804dc0471e4eae5878ae77578984feca47d5cf 100644
--- a/src/components/AppNavigation/Settings/ShortcutOverview.vue
+++ b/src/components/AppNavigation/Settings/ShortcutOverview.vue
@@ -116,6 +116,9 @@ export default {
}, {
keys: [['Ctrl+Delete']],
label: t('calendar', 'Delete edited event'),
+ }, {
+ keys: [['Ctrl+d']],
+ label: t('calendar', 'Duplicate event'),
}],
}]
},
diff --git a/src/components/Appointments/AppointmentDetails.vue b/src/components/Appointments/AppointmentDetails.vue
index 4da794f144538b1866e0e321f0a99354b0caba51..cf7ae7b4f71fc303707fc118fad5b1ecb7455c50 100644
--- a/src/components/Appointments/AppointmentDetails.vue
+++ b/src/components/Appointments/AppointmentDetails.vue
@@ -148,6 +148,7 @@ export default {
display: flex;
flex-direction: row;
padding: 30px;
+ flex-wrap: wrap;
}
.booking-details {
diff --git a/src/components/CalendarGrid.vue b/src/components/CalendarGrid.vue
index dfc6265f60fdeb7d29e8cbc0804f85ef27a3e301..ab34e60289cb655404de1b3223ef89a9a2ad1eec 100644
--- a/src/components/CalendarGrid.vue
+++ b/src/components/CalendarGrid.vue
@@ -131,7 +131,7 @@ export default {
eventDidMount,
noEventsDidMount,
// FIXME: remove title if upstream is fixed (https://github.com/fullcalendar/fullcalendar/issues/6608#issuecomment-954241059)
- eventOrder: ['title', 'start', '-duration', 'allDay', eventOrder],
+ eventOrder: (this.$route.params.view === 'timeGridWeek' ? ['title'] : []).concat(['start', '-duration', 'allDay', eventOrder]),
forceEventDuration: false,
headerToolbar: false,
height: '100%',
diff --git a/src/components/Editor/FreeBusy/FreeBusy.vue b/src/components/Editor/FreeBusy/FreeBusy.vue
index 2457f369d5103a73c7b5da18fc901524d2a85204..dcf2b45e3b640d14ad352d84c72422856a153f79 100644
--- a/src/components/Editor/FreeBusy/FreeBusy.vue
+++ b/src/components/Editor/FreeBusy/FreeBusy.vue
@@ -293,11 +293,6 @@ export default {
::v-deep .mx-input{
height: 38px !important;
}
-
-::v-deep .icon-new-calendar {
- background-color: var(--color-main-background); border: none; padding: 6px; margin-top: 17px;
- cursor: default;
-}
diff --git a/src/mixins/EditorMixin.js b/src/mixins/EditorMixin.js
index 5776b3354768fe913c29547f529ab43ba7eb8521..11a164a8556b917a108f71f487a03e0672f9b2fa 100644
--- a/src/mixins/EditorMixin.js
+++ b/src/mixins/EditorMixin.js
@@ -444,6 +444,14 @@ export default {
this.deleteAndLeave(false)
}
},
+ keyboardDuplicateEvent(event) {
+ if (event.key === 'd' && event.ctrlKey === true) {
+ event.preventDefault()
+ if (!this.isNew && !this.isReadOnly && !this.canCreateRecurrenceException) {
+ this.duplicateEvent()
+ }
+ }
+ },
/**
* Saves a calendar-object
*
@@ -480,6 +488,16 @@ export default {
this.requiresActionOnRouteLeave = false
this.closeEditor()
},
+
+ /**
+ * Duplicates a calendar-object and saves it
+ *
+ * @return {Promise}
+ */
+ async duplicateEvent() {
+ await this.$store.dispatch('duplicateCalendarObjectInstance')
+ },
+
/**
* Deletes a calendar-object
*
diff --git a/src/models/event.js b/src/models/event.js
index f72bc63fd4409061bc586343048cd171dfb64efe..7148deab381528bcad3136c4b8789fa6e49e14c3 100644
--- a/src/models/event.js
+++ b/src/models/event.js
@@ -21,8 +21,8 @@
*/
import { getDateFromDateTimeValue } from '../utils/date.js'
-import { DurationValue } from '@nextcloud/calendar-js'
-import { getHexForColorName } from '../utils/color.js'
+import { DurationValue, DateTimeValue } from '@nextcloud/calendar-js'
+import { getHexForColorName, getClosestCSS3ColorNameForHex } from '../utils/color.js'
import { mapAlarmComponentToAlarmObject } from './alarm.js'
import { mapAttendeePropertyToAttendeeObject } from './attendee.js'
import {
@@ -179,7 +179,53 @@ const mapEventComponentToEventObject = (eventComponent) => {
return eventObject
}
+/**
+ * Copy data from a calendar-object-instance into a calendar-js event-component
+ *
+ * @param {object} eventObject The calendar-object-instance object
+ * @param {EventComponent} eventComponent The calendar-js EventComponent object
+ */
+const copyCalendarObjectInstanceIntoEventComponent = (eventObject, eventComponent) => {
+ eventComponent.title = eventObject.title
+ eventComponent.location = eventObject.location
+ eventComponent.description = eventObject.description
+ eventComponent.accessClass = eventObject.accessClass
+ eventComponent.status = eventObject.status
+ eventComponent.timeTransparency = eventObject.timeTransparency
+
+ for (const category of eventObject.categories) {
+ eventComponent.addCategory(category)
+ }
+
+ if (eventObject.organizer) {
+ eventComponent.setOrganizerFromNameAndEMail(eventObject.organizer.commonName, eventObject.organizer.uri)
+ }
+
+ for (const alarm of eventObject.alarms) {
+ if (alarm.isRelative) {
+ const duration = DurationValue.fromSeconds(alarm.relativeTrigger)
+ eventComponent.addRelativeAlarm(alarm.type, duration, alarm.relativeIsRelatedToStart)
+ } else {
+ const date = DateTimeValue.fromJSDate(alarm.absoluteDate)
+ eventComponent.addAbsoluteAlarm(alarm.type, date)
+ }
+ }
+
+ for (const attendee of eventObject.attendees) {
+ eventComponent.addProperty(attendee.attendeeProperty)
+ }
+
+ for (const rule of eventObject.eventComponent.getPropertyIterator('RRULE')) {
+ eventComponent.addProperty(rule)
+ }
+
+ if (eventObject.customColor) {
+ eventComponent.color = getClosestCSS3ColorNameForHex(eventObject.customColor)
+ }
+}
+
export {
getDefaultEventObject,
mapEventComponentToEventObject,
+ copyCalendarObjectInstanceIntoEventComponent,
}
diff --git a/src/store/calendarObjectInstance.js b/src/store/calendarObjectInstance.js
index a4e584f7e5ebacdc4c36ee7350ba29053c5a3d5e..d43260af09c92f604de87d4797172d69c78c26a6 100644
--- a/src/store/calendarObjectInstance.js
+++ b/src/store/calendarObjectInstance.js
@@ -27,6 +27,7 @@ import {
import { AttendeeProperty, Property, DateTimeValue, DurationValue, RecurValue } from '@nextcloud/calendar-js'
import { getBySetPositionAndBySetFromDate, getWeekDayFromDate } from '../utils/recurrence.js'
import {
+ copyCalendarObjectInstanceIntoEventComponent,
getDefaultEventObject,
mapEventComponentToEventObject,
} from '../models/event.js'
@@ -1567,6 +1568,33 @@ const actions = {
}
},
+ /**
+ * Duplicate calendar-object-instance
+ *
+ * @param {object} vuex The vuex destructuring object
+ * @param {object} vuex.state The Vuex state
+ * @param {Function} vuex.dispatch The Vuex dispatch function
+ * @param {Function} vuex.commit The Vuex commit function
+ * @return {Promise}
+ */
+ async duplicateCalendarObjectInstance({ state, dispatch, commit }) {
+ const oldCalendarObjectInstance = state.calendarObjectInstance
+ const oldEventComponent = oldCalendarObjectInstance.eventComponent
+ const startDate = oldEventComponent.startDate.getInUTC()
+ const endDate = oldEventComponent.endDate.getInUTC()
+ const calendarObject = await dispatch('createNewEvent', {
+ start: startDate.unixTime,
+ end: endDate.unixTime,
+ timezoneId: oldEventComponent.startDate.timezoneId,
+ isAllDay: oldEventComponent.isAllDay(),
+ })
+ const eventComponent = getObjectAtRecurrenceId(calendarObject, startDate.jsDate)
+ copyCalendarObjectInstanceIntoEventComponent(oldCalendarObjectInstance, eventComponent)
+ const calendarObjectInstance = mapEventComponentToEventObject(eventComponent)
+
+ await commit('setCalendarObjectInstanceForNewEvent', { calendarObject, calendarObjectInstance })
+ },
+
/**
* Deletes a calendar-object-instance
*
diff --git a/src/store/calendarObjects.js b/src/store/calendarObjects.js
index 98a4886b800ceb77bf62124028d986770cb239ed..92994f0161a05eb8927965dd171712c4dbdfd1bf 100644
--- a/src/store/calendarObjects.js
+++ b/src/store/calendarObjects.js
@@ -83,6 +83,18 @@ const mutations = {
}
},
+ /**
+ * Updates a calendar-object's calendarId
+ *
+ * @param {object} state The store data
+ * @param {object} data The destructuring object
+ * @param {string} data.calendarObjectId Id of calendar-object to update
+ * @param {string} data.calendarId New calendarId
+ */
+ updateCalendarObjectIdCalendarId(state, { calendarObjectId, calendarId }) {
+ state.calendarObjects[calendarObjectId].calendarId = calendarId
+ },
+
/**
* Resets a calendar-object to it's original server state
*
@@ -165,17 +177,18 @@ const actions = {
return
}
- const newCalendarObject = context.getters.getCalendarById(newCalendarId)
- if (!newCalendarObject) {
+ const newCalendar = context.getters.getCalendarById(newCalendarId)
+ if (!newCalendar) {
logger.error('Calendar to move to not found, aborting …')
return
}
- context.commit('deleteCalendarObject', {
- calendarObject,
+ await calendarObject.dav.move(newCalendar.dav)
+ // Update calendarId in calendarObject manually as it is not stored in dav
+ context.commit('updateCalendarObjectIdCalendarId', {
+ calendarObjectId: calendarObject.id,
+ calendarId: newCalendarId,
})
- await calendarObject.dav.move(newCalendarObject.dav)
- context.commit('appendCalendarObject', { calendarObject })
context.commit('addCalendarObjectToCalendar', {
calendar: {
diff --git a/src/store/settings.js b/src/store/settings.js
index 383610b1125ed909dc712f48b69e666ba0a1c2b2..94fb0d69d1dc1de77a70604e368c2fcd819e9e70 100644
--- a/src/store/settings.js
+++ b/src/store/settings.js
@@ -32,6 +32,7 @@ const state = {
appVersion: null,
firstRun: null,
talkEnabled: false,
+ disableAppointments: false,
// user-defined calendar settings
eventLimit: null,
showTasks: null,
@@ -147,8 +148,9 @@ const mutations = {
* @param {string} data.timezone The timezone to view the calendar in. Either an Olsen timezone or "automatic"
* @param {boolean} data.hideEventExport
* @param {string} data.forceEventAlarmType
+ * @param {boolean} data.disableAppointments Allow to disable the appointments feature
*/
- loadSettingsFromServer(state, { appVersion, eventLimit, firstRun, showWeekNumbers, showTasks, showWeekends, skipPopover, slotDuration, defaultReminder, talkEnabled, tasksEnabled, timezone, hideEventExport, forceEventAlarmType }) {
+ loadSettingsFromServer(state, { appVersion, eventLimit, firstRun, showWeekNumbers, showTasks, showWeekends, skipPopover, slotDuration, defaultReminder, talkEnabled, tasksEnabled, timezone, hideEventExport, forceEventAlarmType, disableAppointments }) {
logInfo(`
Initial settings:
- AppVersion: ${appVersion}
@@ -165,6 +167,7 @@ Initial settings:
- Timezone: ${timezone}
- HideEventExport: ${hideEventExport}
- ForceEventAlarmType: ${forceEventAlarmType}
+ - disableAppointments: ${disableAppointments}
`)
state.appVersion = appVersion
@@ -181,6 +184,7 @@ Initial settings:
state.timezone = timezone
state.hideEventExport = hideEventExport
state.forceEventAlarmType = forceEventAlarmType
+ state.disableAppointments = disableAppointments
},
/**
diff --git a/src/views/Appointments/Booking.vue b/src/views/Appointments/Booking.vue
index 7ac95706be5cd1139256d176d16a8f39369cfca1..5f5df28b9d047e76abfaf82f526eed68a80fff3a 100644
--- a/src/views/Appointments/Booking.vue
+++ b/src/views/Appointments/Booking.vue
@@ -259,10 +259,9 @@ export default {
&__slot-selection {
flex-grow: 2;
}
-
- &__time-zone {
- max-width: 210px;
- }
+ &__time-zone {
+ max-width: 250px;
+ }
&__slots {
display: flex;
diff --git a/src/views/Calendar.vue b/src/views/Calendar.vue
index 0da5ec1dc652460257efc879d99c158fdec26ca0..274b60aa2439f5749123c4cb7ed6a192e04ab3ce 100644
--- a/src/views/Calendar.vue
+++ b/src/views/Calendar.vue
@@ -141,6 +141,7 @@ export default {
showTasks: state => state.settings.showTasks,
timezone: state => state.settings.timezone,
modificationCount: state => state.calendarObjects.modificationCount,
+ disableAppointments: state => state.settings.disableAppointments,
}),
defaultDate() {
return getYYYYMMDDFromFirstdayParam(this.$route.params?.firstDay ?? 'now')
@@ -216,6 +217,7 @@ export default {
showTasks: loadState('calendar', 'show_tasks'),
hideEventExport: loadState('calendar', 'hide_event_export'),
forceEventAlarmType: loadState('calendar', 'force_event_alarm_type', false),
+ disableAppointments: loadState('calendar', 'disable_appointments', false),
})
this.$store.dispatch('initializeCalendarJsConfig')
diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue
index c0daf2ea17541f0735c9ab43840141c6b82dc595..446eeed6d91c22e08851ffda7c0033813950797a 100644
--- a/src/views/Dashboard.vue
+++ b/src/views/Dashboard.vue
@@ -50,8 +50,10 @@
-
+
+
+
+
{{ t('calendar', 'No upcoming events') }}
@@ -66,6 +68,7 @@
|