From ce4070bd51dec239e9a6ad792447116fe705302d Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Mon, 10 Aug 2026 12:35:44 -0400 Subject: [PATCH 1/3] fix: allways allow duplicaiton Signed-off-by: SebastianKrupinski --- src/mixins/EditorMixin.js | 14 ++- src/models/event.js | 50 ++++++++--- src/store/calendarObjectInstance.js | 6 +- src/views/EditFull.vue | 2 +- src/views/EditSimple.vue | 2 +- tests/javascript/unit/models/event.test.js | 66 +++++++++++++- .../unit/store/calendarObjectInstance.test.ts | 88 +++++++++++++++++++ 7 files changed, 206 insertions(+), 22 deletions(-) create mode 100644 tests/javascript/unit/store/calendarObjectInstance.test.ts diff --git a/src/mixins/EditorMixin.js b/src/mixins/EditorMixin.js index 635341cf61..79e6828dde 100644 --- a/src/mixins/EditorMixin.js +++ b/src/mixins/EditorMixin.js @@ -645,7 +645,7 @@ export default { keyboardDuplicateEvent(event) { if (event.key === 'd' && event.ctrlKey === true) { event.preventDefault() - if (!this.isNew && !this.isReadOnly && !this.canCreateRecurrenceException) { + if (!this.isNew && !this.canCreateRecurrenceException) { this.duplicateEvent() } } @@ -701,12 +701,20 @@ export default { }, /** - * Duplicates a calendar-object and saves it + * Duplicates the calendar-object. If the source calendar is + * read-only, the duplicate is created in the first writable calendar. * * @return {Promise} */ async duplicateEvent() { - await this.calendarObjectInstanceStore.duplicateCalendarObjectInstance() + const calendarId = this.isReadOnly + ? (this.calendarsStore.sortedCalendars[0]?.id ?? null) + : (this.calendarObject?.calendarId ?? null) + await this.calendarObjectInstanceStore.duplicateCalendarObjectInstance({ calendarId }) + + // The editor's calendar picker is driven by this.calendarId, which is + // separate from the store's calendarObject.calendarId. + this.calendarId = this.calendarObject?.calendarId ?? null }, /** diff --git a/src/models/event.js b/src/models/event.js index 3a2adab38e..1b9640e7c6 100644 --- a/src/models/event.js +++ b/src/models/event.js @@ -191,18 +191,6 @@ function mapEventComponentToEventObject(eventComponent) { function copyCalendarObjectInstanceIntoEventComponent(eventObject, eventComponent) { const sourceEventComponent = eventObject.eventComponent - const unexpectedRecurrenceProperties = new Set([ - 'RRULE', - 'EXRULE', - 'RDATE', - 'EXDATE', - ]) - for (const property of sourceEventComponent.getPropertyIterator()) { - if (unexpectedRecurrenceProperties.has(property.name)) { - throw new Error(`Illegal argument: Event objects has recurrence related property ${property.name}.`) - } - } - const propertiesExcludedFromCopying = new Set([ // These properties are regenerated for the new copy. 'UID', @@ -213,11 +201,49 @@ function copyCalendarObjectInstanceIntoEventComponent(eventObject, eventComponen // Currently only copying as exact occurrences. // Therefore, do not preserve any RECURRENCE-ID. 'RECURRENCE-ID', + // A duplicate is always a single, standalone event. If the source is + // the master item of a recurring series (e.g. when duplicating its + // first occurrence, calendar-js returns the master item itself), + // its recurrence-defining properties must not carry over. + 'RRULE', + 'EXRULE', + 'RDATE', + 'EXDATE', + ]) + + // Properties that must not occur more than once on a VEVENT (RFC 5545 + // section 3.6.1) but that the source may legitimately carry a value for. + const propertiesReplacedWhenCopying = new Set([ + 'DTSTART', + 'DTEND', + 'DURATION', + 'CLASS', + 'DESCRIPTION', + 'GEO', + 'LOCATION', + 'ORGANIZER', + 'PRIORITY', + 'STATUS', + 'SUMMARY', + 'TRANSP', + 'URL', ]) + + // DTEND and DURATION are mutually exclusive. If the source uses DURATION, + // a pre-existing target DTEND (or vice versa) would otherwise be left + // behind alongside it, which is invalid. + if (sourceEventComponent.hasProperty('DTEND') || sourceEventComponent.hasProperty('DURATION')) { + eventComponent.deleteAllProperties('DTEND') + eventComponent.deleteAllProperties('DURATION') + } + for (const property of sourceEventComponent.getPropertyIterator()) { if (propertiesExcludedFromCopying.has(property.name)) { continue } + if (propertiesReplacedWhenCopying.has(property.name)) { + eventComponent.deleteAllProperties(property.name) + } const successful = eventComponent.addProperty(property.clone()) if (!successful) { throw new Error(`Illegal state: Property ${property.name} could not be copied.`) diff --git a/src/store/calendarObjectInstance.js b/src/store/calendarObjectInstance.js index aa532d8668..d64a8dc465 100644 --- a/src/store/calendarObjectInstance.js +++ b/src/store/calendarObjectInstance.js @@ -1425,9 +1425,11 @@ export default defineStore('calendarObjectInstance', { /** * Duplicate calendar-object-instance * + * @param {object} data The destructuring object + * @param {string=} data.calendarId The id of the calendar to duplicate the event into. Defaults to the source event's calendar * @return {Promise} */ - async duplicateCalendarObjectInstance() { + async duplicateCalendarObjectInstance({ calendarId } = {}) { const calendarObjectsStore = useCalendarObjectsStore() const oldCalendarObjectInstance = this.calendarObjectInstance @@ -1439,7 +1441,7 @@ export default defineStore('calendarObjectInstance', { end: endDate.unixTime, timezoneId: oldEventComponent.startDate.timezoneId, isAllDay: oldEventComponent.isAllDay(), - calendarId: this.calendarObject?.calendarId ?? null, + calendarId: calendarId ?? this.calendarObject?.calendarId ?? null, }) const eventComponent = getObjectAtRecurrenceId(calendarObject, startDate.jsDate) copyCalendarObjectInstanceIntoEventComponent(oldCalendarObjectInstance, eventComponent) diff --git a/src/views/EditFull.vue b/src/views/EditFull.vue index fbb7a42425..a061d18a9f 100644 --- a/src/views/EditFull.vue +++ b/src/views/EditFull.vue @@ -69,7 +69,7 @@ {{ $t('calendar', 'Export') }} - + diff --git a/src/views/EditSimple.vue b/src/views/EditSimple.vue index 4c737988fa..6981dd1ad5 100644 --- a/src/views/EditSimple.vue +++ b/src/views/EditSimple.vue @@ -82,7 +82,7 @@ {{ $t('calendar', 'Export') }} - + diff --git a/tests/javascript/unit/models/event.test.js b/tests/javascript/unit/models/event.test.js index dbaab9876a..f2f830c3ee 100644 --- a/tests/javascript/unit/models/event.test.js +++ b/tests/javascript/unit/models/event.test.js @@ -967,6 +967,55 @@ describe('Test suite: Event model (models/event.js)', () => { expect(targetEventComponent.getFirstPropertyFirstValue('A-CUSTOM-PROPERTY')).toBe('TRUE') }) + it('should replace, not duplicate, DTSTART/DTEND in the new event component', () => { + // Given + // The target already has its own DTSTART/DTEND set from the occurrence + // it was created for. Simply appending the source's on top would leave + // the component with duplicate DTSTART/DTEND properties, which servers + // reject as invalid iCalendar - so the target's own values must be + // replaced by the source's, not added alongside them. + const sourceRecurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 7, 0, 0)), true) + const sourceEventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-timed', sourceRecurrenceId) + const sourceEventObject = mapEventComponentToEventObject(sourceEventComponent) + + const targetRecurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 9, 0, 0)), true) + const targetEventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-minimal', targetRecurrenceId) + + // When + copyCalendarObjectInstanceIntoEventComponent(sourceEventObject, targetEventComponent) + + // Then + expect([...targetEventComponent.getPropertyIterator('DTSTART')]).toHaveLength(1) + expect([...targetEventComponent.getPropertyIterator('DTEND')]).toHaveLength(1) + expect(targetEventComponent.startDate.unixTime).toEqual(sourceEventComponent.startDate.unixTime) + expect(targetEventComponent.endDate.unixTime).toEqual(sourceEventComponent.endDate.unixTime) + }) + + it('should not leave a stale DTEND when the source uses DURATION instead', () => { + // Given + // DTEND and DURATION are mutually exclusive on a VEVENT. The target + // (created via calendar-js's createEvent()) always has a DTEND, so if + // the source describes its length with DURATION instead, the target's + // DTEND must still be cleared - not just left behind alongside the + // newly-added DURATION. + const sourceRecurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 7, 0, 0)), true) + const sourceEventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-timed', sourceRecurrenceId) + sourceEventComponent.deleteAllProperties('DTEND') + sourceEventComponent.updatePropertyWithValue('DURATION', DurationValue.fromSeconds(3600)) + const sourceEventObject = mapEventComponentToEventObject(sourceEventComponent) + + const targetRecurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 9, 0, 0)), true) + const targetEventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-minimal', targetRecurrenceId) + expect(targetEventComponent.hasProperty('DTEND')).toBe(true) + + // When + copyCalendarObjectInstanceIntoEventComponent(sourceEventObject, targetEventComponent) + + // Then + expect([...targetEventComponent.getPropertyIterator('DTEND')]).toHaveLength(0) + expect([...targetEventComponent.getPropertyIterator('DURATION')]).toHaveLength(1) + }) + it('should not copy recurrence ID into a new event component', () => { // Given const sourceRecurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2020, 2, 8, 14, 0, 0)), true) @@ -983,8 +1032,12 @@ describe('Test suite: Event model (models/event.js)', () => { expect(targetEventComponent.hasProperty('RECURRENCE-ID')).toBeFalsy() }) - it('should not copy recurring events into a new event component', () => { + it('should not copy recurrence-defining properties into a new event component', () => { // Given + // The first occurrence's recurrence-id matches the master item's own DTSTART, + // so calendar-js returns the master item itself here (RRULE and all) instead + // of a forked occurrence. A duplicate must still come out as a single, + // non-recurring event rather than throwing or inheriting the recurrence rule. const sourceRecurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2020, 2, 1, 14, 0, 0)), true) const sourceEventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-recurring', sourceRecurrenceId) const sourceEventObject = mapEventComponentToEventObject(sourceEventComponent) @@ -993,8 +1046,15 @@ describe('Test suite: Event model (models/event.js)', () => { const targetEventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-minimal', targetRecurrenceId) // When - expect(() => copyCalendarObjectInstanceIntoEventComponent(sourceEventObject, targetEventComponent)) - .toThrow('Illegal argument: Event objects has recurrence related property RRULE.') + copyCalendarObjectInstanceIntoEventComponent(sourceEventObject, targetEventComponent) + + // Then + expect(targetEventComponent.hasProperty('RRULE')).toBeFalsy() + expect(targetEventComponent.hasProperty('EXRULE')).toBeFalsy() + expect(targetEventComponent.hasProperty('RDATE')).toBeFalsy() + expect(targetEventComponent.hasProperty('EXDATE')).toBeFalsy() + // Non-recurrence properties from the source are still copied as usual. + expect(targetEventComponent.title).toEqual(sourceEventComponent.title) }) it('should copy subcomponents into a new event component', () => { diff --git a/tests/javascript/unit/store/calendarObjectInstance.test.ts b/tests/javascript/unit/store/calendarObjectInstance.test.ts new file mode 100644 index 0000000000..cda4df323a --- /dev/null +++ b/tests/javascript/unit/store/calendarObjectInstance.test.ts @@ -0,0 +1,88 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { createPinia, setActivePinia } from 'pinia' +import { describe, expect, it, vi } from 'vitest' +import { copyCalendarObjectInstanceIntoEventComponent, mapEventComponentToEventObject } from '@/models/event.js' +import useCalendarObjectInstanceStore from '@/store/calendarObjectInstance.js' +import useCalendarObjectsStore from '@/store/calendarObjects.js' +import { getObjectAtRecurrenceId } from '@/utils/calendarObject.js' + +vi.mock('@/models/event.js') +vi.mock('@/utils/calendarObject.js') + +const mockedCopyCalendarObjectInstanceIntoEventComponent = vi.mocked(copyCalendarObjectInstanceIntoEventComponent) +const mockedMapEventComponentToEventObject = vi.mocked(mapEventComponentToEventObject) +const mockedGetObjectAtRecurrenceId = vi.mocked(getObjectAtRecurrenceId) + +describe('store/calendarObjectInstance test suite', () => { + beforeEach(() => { + setActivePinia(createPinia()) + + mockedCopyCalendarObjectInstanceIntoEventComponent.mockReset() + mockedMapEventComponentToEventObject.mockReset().mockReturnValue({ eventComponent: {} }) + mockedGetObjectAtRecurrenceId.mockReset().mockReturnValue({}) + }) + + describe('duplicateCalendarObjectInstance', () => { + /** + * @param store The calendarObjectInstance store + * @param calendarId The id of the calendar the source event lives in + */ + function setUpSourceEvent(store: ReturnType, calendarId: string) { + store.calendarObject = { calendarId } + store.calendarObjectInstance = { + eventComponent: { + startDate: { + timezoneId: 'UTC', + getInUTC: () => ({ unixTime: 1000, jsDate: new Date(1000 * 1000) }), + }, + endDate: { + getInUTC: () => ({ unixTime: 2000 }), + }, + isAllDay: () => false, + }, + } + } + + it('duplicates into the explicitly given calendar instead of the source calendar', async () => { + const store = useCalendarObjectInstanceStore() + const calendarObjectsStore = useCalendarObjectsStore() + setUpSourceEvent(store, 'readonly-calendar') + vi.spyOn(calendarObjectsStore, 'createNewEvent').mockResolvedValue({ calendarComponent: {} }) + + await store.duplicateCalendarObjectInstance({ calendarId: 'writable-calendar' }) + + expect(calendarObjectsStore.createNewEvent).toHaveBeenCalledWith( + expect.objectContaining({ calendarId: 'writable-calendar' }), + ) + }) + + it('falls back to the source calendar when no calendarId is given', async () => { + const store = useCalendarObjectInstanceStore() + const calendarObjectsStore = useCalendarObjectsStore() + setUpSourceEvent(store, 'source-calendar') + vi.spyOn(calendarObjectsStore, 'createNewEvent').mockResolvedValue({ calendarComponent: {} }) + + await store.duplicateCalendarObjectInstance() + + expect(calendarObjectsStore.createNewEvent).toHaveBeenCalledWith( + expect.objectContaining({ calendarId: 'source-calendar' }), + ) + }) + + it('marks the duplicated event as a new, unsaved calendar-object', async () => { + const store = useCalendarObjectInstanceStore() + const calendarObjectsStore = useCalendarObjectsStore() + setUpSourceEvent(store, 'source-calendar') + const newCalendarObject = { calendarComponent: {} } + vi.spyOn(calendarObjectsStore, 'createNewEvent').mockResolvedValue(newCalendarObject) + + await store.duplicateCalendarObjectInstance({ calendarId: 'writable-calendar' }) + + expect(store.isNew).toBe(true) + expect(store.calendarObject).toStrictEqual(newCalendarObject) + }) + }) +}) From edc5724bba9f10c3cfc80d0eff1d888a9a8ff354 Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Tue, 11 Aug 2026 20:36:38 -0400 Subject: [PATCH 2/3] fixup! fix: allways allow duplicaiton Signed-off-by: SebastianKrupinski --- src/fullcalendar/interaction/eventClick.js | 5 +- src/mixins/EditorMixin.js | 23 +++- src/store/calendarObjectInstance.js | 6 +- src/utils/router.js | 41 ++++++- src/views/Calendar.vue | 30 +++--- src/views/EditFull.vue | 2 +- src/views/EditSimple.vue | 2 +- .../interaction/eventClick.test.js | 24 +++-- .../unit/mixins/EditorMixin.test.js | 100 +++++++++++++++++- .../unit/store/calendarObjectInstance.test.ts | 13 --- tests/javascript/unit/utils/router.test.js | 29 +++-- 11 files changed, 214 insertions(+), 61 deletions(-) diff --git a/src/fullcalendar/interaction/eventClick.js b/src/fullcalendar/interaction/eventClick.js index 4e7984c6d8..eddb65e3ad 100644 --- a/src/fullcalendar/interaction/eventClick.js +++ b/src/fullcalendar/interaction/eventClick.js @@ -11,7 +11,8 @@ import useSettingsStore from '@/store/settings.js' import useWidgetStore from '@/store/widget.js' import { getPrefixedRoute, - isPublicOrEmbeddedRoute, + getViewMode, + ViewMode, } from '@/utils/router.js' /** @@ -99,7 +100,7 @@ function handleEventClick(event, router, route, window, isWidget = false) { function handleToDoClick(event, route, window, isWidget = false) { const settingsStore = useSettingsStore() - if (isWidget || isPublicOrEmbeddedRoute(route.name)) { + if (getViewMode(route.name, isWidget) !== ViewMode.USER) { return } diff --git a/src/mixins/EditorMixin.js b/src/mixins/EditorMixin.js index 79e6828dde..636d787636 100644 --- a/src/mixins/EditorMixin.js +++ b/src/mixins/EditorMixin.js @@ -20,7 +20,7 @@ import { removeMailtoPrefix } from '@/utils/attendee.js' import { uidToHexColor } from '@/utils/color.js' import { dateFactory } from '@/utils/date.js' import logger from '@/utils/logger.js' -import { getPrefixedRoute } from '@/utils/router.js' +import { getPrefixedRoute, getViewMode, ViewMode } from '@/utils/router.js' /** * This is a mixin for the editor. It contains common Vue stuff, that is @@ -293,6 +293,21 @@ export default { } return this.principalsStore.getPrincipalByUrl(this.selectedCalendar.delegatorUrl)?.userId ?? null }, + /** + * Returns the mode the editor is currently rendered in + * (authenticated user, public share, embedded share, or widget). + * + * @return {string} One of ViewMode + */ + viewMode() { + return getViewMode(this.$route?.name, this.isWidget) + }, + /** + * @return {boolean} + */ + canDuplicate() { + return this.viewMode === ViewMode.USER + }, /** * Returns whether or not the user is allowed to delete this event * @@ -645,7 +660,7 @@ export default { keyboardDuplicateEvent(event) { if (event.key === 'd' && event.ctrlKey === true) { event.preventDefault() - if (!this.isNew && !this.canCreateRecurrenceException) { + if (!this.isNew && this.canDuplicate) { this.duplicateEvent() } } @@ -707,6 +722,10 @@ export default { * @return {Promise} */ async duplicateEvent() { + if (!this.canDuplicate) { + return + } + const calendarId = this.isReadOnly ? (this.calendarsStore.sortedCalendars[0]?.id ?? null) : (this.calendarObject?.calendarId ?? null) diff --git a/src/store/calendarObjectInstance.js b/src/store/calendarObjectInstance.js index d64a8dc465..4f24b06856 100644 --- a/src/store/calendarObjectInstance.js +++ b/src/store/calendarObjectInstance.js @@ -1426,10 +1426,10 @@ export default defineStore('calendarObjectInstance', { * Duplicate calendar-object-instance * * @param {object} data The destructuring object - * @param {string=} data.calendarId The id of the calendar to duplicate the event into. Defaults to the source event's calendar + * @param {string} data.calendarId The id of the calendar to duplicate the event into. Must be a writable calendar * @return {Promise} */ - async duplicateCalendarObjectInstance({ calendarId } = {}) { + async duplicateCalendarObjectInstance({ calendarId }) { const calendarObjectsStore = useCalendarObjectsStore() const oldCalendarObjectInstance = this.calendarObjectInstance @@ -1441,7 +1441,7 @@ export default defineStore('calendarObjectInstance', { end: endDate.unixTime, timezoneId: oldEventComponent.startDate.timezoneId, isAllDay: oldEventComponent.isAllDay(), - calendarId: calendarId ?? this.calendarObject?.calendarId ?? null, + calendarId, }) const eventComponent = getObjectAtRecurrenceId(calendarObject, startDate.jsDate) copyCalendarObjectInstanceIntoEventComponent(oldCalendarObjectInstance, eventComponent) diff --git a/src/utils/router.js b/src/utils/router.js index dc4067db25..d99561114e 100644 --- a/src/utils/router.js +++ b/src/utils/router.js @@ -93,11 +93,42 @@ export function getPrefixedRoute(currentRouteName, toRouteName) { } /** - * Checks whether a routeName represents a public / embedded route + * The different modes the calendar app can be rendered in. + * This is the single source of truth for what "public", "embedded" + * and "widget" mean across the app - components should derive their + * behaviour from this instead of re-deriving it from route names or + * calendar permissions. + */ +export const ViewMode = Object.freeze({ + // A normal, authenticated user viewing their own calendars + USER: 'user', + // A public share link (/p/:tokens/...) + PUBLIC: 'public', + // An embedded share link (/embed/:tokens/...) + EMBEDDED: 'embedded', + // A dashboard / Talk / Text reference widget (no vue-router present) + WIDGET: 'widget', +}) + +/** + * Determines the view mode the calendar is currently rendered in. * - * @param {string} routeName Name of the route - * @return {boolean} + * @param {string|undefined|null} routeName Name of the current vue-router route, if any + * @param {boolean} isWidget Whether the calendar is rendered as a reference widget + * @return {string} One of ViewMode */ -export function isPublicOrEmbeddedRoute(routeName) { - return routeName.startsWith('Embed') || routeName.startsWith('Public') +export function getViewMode(routeName, isWidget = false) { + if (isWidget) { + return ViewMode.WIDGET + } + + if (routeName?.startsWith('Embed')) { + return ViewMode.EMBEDDED + } + + if (routeName?.startsWith('Public')) { + return ViewMode.PUBLIC + } + + return ViewMode.USER } diff --git a/src/views/Calendar.vue b/src/views/Calendar.vue index 53dbc4b9d7..183f203461 100644 --- a/src/views/Calendar.vue +++ b/src/views/Calendar.vue @@ -147,6 +147,7 @@ import { import logger from '@/utils/logger.js' import loadMomentLocalization from '@/utils/moment.js' import { isAfterVersion } from '@/utils/nextcloudVersion.ts' +import { getViewMode, ViewMode } from '@/utils/router.js' import '@nextcloud/dialogs/style.css' @@ -237,35 +238,36 @@ export default { return getYYYYMMDDFromFirstdayParam(this.$route?.params?.firstDay ?? 'now') }, + // The mode this calendar is currently rendered in. This is the single + // source of truth for public/embedded/widget state + viewMode() { + if (this.isWidget) { + return this.isPublic ? ViewMode.PUBLIC : ViewMode.WIDGET + } + return getViewMode(this.$route?.name) + }, + isEditable() { // We do not allow drag and drop when the editor is open. - return !this.isPublicShare - && !this.isEmbedded - && !this.isWidget + return this.isAuthenticatedUser && this.$route?.name !== 'EditPopoverView' && this.$route?.name !== 'EditFullView' }, isSelectable() { - return !this.isPublicShare && !this.isEmbedded && !this.isWidget + return this.isAuthenticatedUser }, isAuthenticatedUser() { - return !this.isPublicShare && !this.isEmbedded && !this.isWidget + return this.viewMode === ViewMode.USER }, isPublicShare() { - if (this.isWidget) { - return false - } - return this.$route.name.startsWith('Public') + return this.viewMode === ViewMode.PUBLIC }, isEmbedded() { - if (this.isWidget) { - return false - } - return this.$route.name.startsWith('Embed') + return this.viewMode === ViewMode.EMBEDDED }, showWidgetEventDetails() { @@ -348,7 +350,7 @@ export default { }) this.settingsStore.initializeCalendarJsConfig() - if (this.$route?.name.startsWith('Public') || this.$route?.name.startsWith('Embed') || this.isPublic) { + if (this.viewMode === ViewMode.PUBLIC || this.viewMode === ViewMode.EMBEDDED) { await initializeClientForPublicView() const tokens = this.isWidget ? [this.referenceToken] : this.$route.params.tokens.split('-') const calendars = await this.calendarsStore.getPublicCalendars({ tokens }) diff --git a/src/views/EditFull.vue b/src/views/EditFull.vue index a061d18a9f..1a4b7ebfc1 100644 --- a/src/views/EditFull.vue +++ b/src/views/EditFull.vue @@ -69,7 +69,7 @@ {{ $t('calendar', 'Export') }} - + diff --git a/src/views/EditSimple.vue b/src/views/EditSimple.vue index 6981dd1ad5..353f91280e 100644 --- a/src/views/EditSimple.vue +++ b/src/views/EditSimple.vue @@ -82,7 +82,7 @@ {{ $t('calendar', 'Export') }} - + diff --git a/tests/javascript/unit/fullcalendar/interaction/eventClick.test.js b/tests/javascript/unit/fullcalendar/interaction/eventClick.test.js index c4e0a2e17f..11d6620cc6 100644 --- a/tests/javascript/unit/fullcalendar/interaction/eventClick.test.js +++ b/tests/javascript/unit/fullcalendar/interaction/eventClick.test.js @@ -11,7 +11,8 @@ import EditorMixin from '@/mixins/EditorMixin.js' import useSettingsStore from '@/store/settings.js' import { getPrefixedRoute, - isPublicOrEmbeddedRoute, + getViewMode, + ViewMode, } from '@/utils/router.js' vi.mock('@/utils/router.js') @@ -22,7 +23,10 @@ vi.mock('@nextcloud/dialogs') describe('fullcalendar/eventClick test suite', () => { beforeEach(() => { getPrefixedRoute.mockClear() - isPublicOrEmbeddedRoute.mockClear() + getViewMode.mockClear() + // Default to a normal authenticated view so tests that don't care + // about public/embedded/widget behaviour don't need to set this up. + getViewMode.mockReturnValue(ViewMode.USER) generateUrl.mockClear() translate.mockClear() showInfo.mockClear() @@ -349,8 +353,8 @@ describe('fullcalendar/eventClick test suite', () => { } const oldLocation = window.location - isPublicOrEmbeddedRoute - .mockReturnValueOnce(true) + getViewMode + .mockReturnValueOnce(ViewMode.PUBLIC) const eventClickFunction = eventClick(router, route, window) eventClickFunction({ event: { @@ -362,8 +366,8 @@ describe('fullcalendar/eventClick test suite', () => { }, }}) - expect(isPublicOrEmbeddedRoute).toHaveBeenCalledTimes(1) - expect(isPublicOrEmbeddedRoute).toHaveBeenNthCalledWith(1, 'EditFullView') + expect(getViewMode).toHaveBeenCalledTimes(1) + expect(getViewMode).toHaveBeenNthCalledWith(1, 'EditFullView', false) expect(generateUrl).toHaveBeenCalledTimes(0) expect(window.location).toEqual(oldLocation) @@ -391,8 +395,8 @@ describe('fullcalendar/eventClick test suite', () => { } const oldLocation = window.location - isPublicOrEmbeddedRoute - .mockReturnValueOnce(false) + getViewMode + .mockReturnValueOnce(ViewMode.USER) translate .mockReturnValue('translated hint') @@ -412,8 +416,8 @@ describe('fullcalendar/eventClick test suite', () => { expect(showInfo).toHaveBeenCalledTimes(1) expect(showInfo).toHaveBeenNthCalledWith(1, 'translated hint') - expect(isPublicOrEmbeddedRoute).toHaveBeenCalledTimes(1) - expect(isPublicOrEmbeddedRoute).toHaveBeenNthCalledWith(1, 'EditFullView') + expect(getViewMode).toHaveBeenCalledTimes(1) + expect(getViewMode).toHaveBeenNthCalledWith(1, 'EditFullView', false) expect(generateUrl).toHaveBeenCalledTimes(0) expect(window.location).toEqual(oldLocation) diff --git a/tests/javascript/unit/mixins/EditorMixin.test.js b/tests/javascript/unit/mixins/EditorMixin.test.js index 43645cda2b..5886dd07be 100644 --- a/tests/javascript/unit/mixins/EditorMixin.test.js +++ b/tests/javascript/unit/mixins/EditorMixin.test.js @@ -2,9 +2,105 @@ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ +import EditorMixin from '../../../../src/mixins/EditorMixin.js' +import { ViewMode } from '../../../../src/utils/router.js' describe('mixins/EditorMixin test suite', () => { - it('should be true', () => { - expect(true).toEqual(true) + describe('viewMode', () => { + it('is WIDGET whenever rendered as a widget, regardless of route', () => { + const vm = { isWidget: true, $route: { name: 'PublicCalendarView' } } + expect(EditorMixin.computed.viewMode.call(vm)).toEqual(ViewMode.WIDGET) + }) + + it('is derived from the route name otherwise', () => { + expect(EditorMixin.computed.viewMode.call({ isWidget: false, $route: { name: 'PublicEditPopoverView' } })).toEqual(ViewMode.PUBLIC) + expect(EditorMixin.computed.viewMode.call({ isWidget: false, $route: { name: 'EmbedEditFullView' } })).toEqual(ViewMode.EMBEDDED) + expect(EditorMixin.computed.viewMode.call({ isWidget: false, $route: { name: 'EditPopoverView' } })).toEqual(ViewMode.USER) + }) + }) + + describe('canDuplicate', () => { + it('is true only in USER mode', () => { + expect(EditorMixin.computed.canDuplicate.call({ viewMode: ViewMode.USER })).toEqual(true) + expect(EditorMixin.computed.canDuplicate.call({ viewMode: ViewMode.PUBLIC })).toEqual(false) + expect(EditorMixin.computed.canDuplicate.call({ viewMode: ViewMode.EMBEDDED })).toEqual(false) + expect(EditorMixin.computed.canDuplicate.call({ viewMode: ViewMode.WIDGET })).toEqual(false) + }) + }) + + describe('duplicateEvent', () => { + it('does nothing when duplication is not allowed in the current view (e.g. public/embedded/widget)', async () => { + const duplicateCalendarObjectInstance = vi.fn() + const vm = { + canDuplicate: false, + calendarObjectInstanceStore: { duplicateCalendarObjectInstance }, + } + + await EditorMixin.methods.duplicateEvent.call(vm) + + expect(duplicateCalendarObjectInstance).not.toHaveBeenCalled() + }) + + it('duplicates into the current calendar when it is writable', async () => { + const duplicateCalendarObjectInstance = vi.fn() + const vm = { + canDuplicate: true, + isReadOnly: false, + calendarObject: { calendarId: 'calendar-1' }, + calendarObjectInstanceStore: { duplicateCalendarObjectInstance }, + calendarsStore: { sortedCalendars: [] }, + } + + await EditorMixin.methods.duplicateEvent.call(vm) + + expect(duplicateCalendarObjectInstance).toHaveBeenCalledWith({ calendarId: 'calendar-1' }) + }) + + it('falls back to the first writable calendar when the source calendar is read-only', async () => { + const duplicateCalendarObjectInstance = vi.fn() + const vm = { + canDuplicate: true, + isReadOnly: true, + calendarObject: { calendarId: 'calendar-1' }, + calendarObjectInstanceStore: { duplicateCalendarObjectInstance }, + calendarsStore: { sortedCalendars: [{ id: 'calendar-2' }] }, + } + + await EditorMixin.methods.duplicateEvent.call(vm) + + expect(duplicateCalendarObjectInstance).toHaveBeenCalledWith({ calendarId: 'calendar-2' }) + }) + }) + + describe('keyboardDuplicateEvent', () => { + it('does not trigger a duplication when it is not allowed in the current view', () => { + const duplicateEvent = vi.fn() + const vm = { + isNew: false, + canCreateRecurrenceException: false, + canDuplicate: false, + duplicateEvent, + } + const event = { key: 'd', ctrlKey: true, preventDefault: vi.fn() } + + EditorMixin.methods.keyboardDuplicateEvent.call(vm, event) + + expect(duplicateEvent).not.toHaveBeenCalled() + }) + + it('triggers a duplication when allowed', () => { + const duplicateEvent = vi.fn() + const vm = { + isNew: false, + canCreateRecurrenceException: false, + canDuplicate: true, + duplicateEvent, + } + const event = { key: 'd', ctrlKey: true, preventDefault: vi.fn() } + + EditorMixin.methods.keyboardDuplicateEvent.call(vm, event) + + expect(duplicateEvent).toHaveBeenCalled() + }) }) }) diff --git a/tests/javascript/unit/store/calendarObjectInstance.test.ts b/tests/javascript/unit/store/calendarObjectInstance.test.ts index cda4df323a..e356ef236d 100644 --- a/tests/javascript/unit/store/calendarObjectInstance.test.ts +++ b/tests/javascript/unit/store/calendarObjectInstance.test.ts @@ -59,19 +59,6 @@ describe('store/calendarObjectInstance test suite', () => { ) }) - it('falls back to the source calendar when no calendarId is given', async () => { - const store = useCalendarObjectInstanceStore() - const calendarObjectsStore = useCalendarObjectsStore() - setUpSourceEvent(store, 'source-calendar') - vi.spyOn(calendarObjectsStore, 'createNewEvent').mockResolvedValue({ calendarComponent: {} }) - - await store.duplicateCalendarObjectInstance() - - expect(calendarObjectsStore.createNewEvent).toHaveBeenCalledWith( - expect.objectContaining({ calendarId: 'source-calendar' }), - ) - }) - it('marks the duplicated event as a new, unsaved calendar-object', async () => { const store = useCalendarObjectInstanceStore() const calendarObjectsStore = useCalendarObjectsStore() diff --git a/tests/javascript/unit/utils/router.test.js b/tests/javascript/unit/utils/router.test.js index 45f65be6af..d855be5e3c 100644 --- a/tests/javascript/unit/utils/router.test.js +++ b/tests/javascript/unit/utils/router.test.js @@ -7,7 +7,8 @@ import { getInitialView, getPreferredEditorRoute, getPrefixedRoute, - isPublicOrEmbeddedRoute, + getViewMode, + ViewMode, } from '@/utils/router.js' vi.mock('@nextcloud/initial-state') @@ -77,14 +78,26 @@ describe('utils/router test suite', () => { expect(getPrefixedRoute('EditPopoverView', 'CalendarView')).toEqual('CalendarView') }) - it('should check whether a route is public or embedded', () => { - expect(isPublicOrEmbeddedRoute('PublicCalendarView')).toEqual(true) - expect(isPublicOrEmbeddedRoute('PublicEditPopoverView')).toEqual(true) + it('should always report widget mode when isWidget is true, regardless of route', () => { + expect(getViewMode('PublicCalendarView', true)).toEqual(ViewMode.WIDGET) + expect(getViewMode('EmbedCalendarView', true)).toEqual(ViewMode.WIDGET) + expect(getViewMode('CalendarView', true)).toEqual(ViewMode.WIDGET) + expect(getViewMode(undefined, true)).toEqual(ViewMode.WIDGET) + }) + + it('should derive the view mode from the route name when not a widget', () => { + expect(getViewMode('PublicCalendarView')).toEqual(ViewMode.PUBLIC) + expect(getViewMode('PublicEditPopoverView')).toEqual(ViewMode.PUBLIC) - expect(isPublicOrEmbeddedRoute('EmbedCalendarView')).toEqual(true) - expect(isPublicOrEmbeddedRoute('EmbedEditPopoverView')).toEqual(true) + expect(getViewMode('EmbedCalendarView')).toEqual(ViewMode.EMBEDDED) + expect(getViewMode('EmbedEditFullView')).toEqual(ViewMode.EMBEDDED) + + expect(getViewMode('CalendarView')).toEqual(ViewMode.USER) + expect(getViewMode('EditPopoverView')).toEqual(ViewMode.USER) + }) - expect(isPublicOrEmbeddedRoute('CalendarView')).toEqual(false) - expect(isPublicOrEmbeddedRoute('EditPopoverView')).toEqual(false) + it('should default to user mode when there is no route name', () => { + expect(getViewMode(undefined)).toEqual(ViewMode.USER) + expect(getViewMode(null)).toEqual(ViewMode.USER) }) }) From 3559cd2a46ab3f6d4d688d70c6ac0b541e7513a4 Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Wed, 19 Aug 2026 08:44:19 -0400 Subject: [PATCH 3/3] fix: use proper view mode and object permissions Signed-off-by: SebastianKrupinski --- src/components/AppNavigation/CalendarList.vue | 2 +- src/components/AppNavigation/Settings.vue | 2 +- src/fullcalendar/eventSources/eventSource.js | 2 +- src/mixins/EditorMixin.js | 5 +++-- src/models/calendar.js | 4 ---- src/store/calendars.js | 8 ++++---- src/store/delegation.ts | 7 ++++--- src/types/calendar.ts | 2 -- src/views/Calendar.vue | 2 +- .../eventSources/eventSource.test.js | 18 ++++++++++++------ tests/javascript/unit/models/calendar.test.js | 13 ------------- tests/javascript/unit/store/calendars.test.js | 7 ++++++- 12 files changed, 33 insertions(+), 39 deletions(-) diff --git a/src/components/AppNavigation/CalendarList.vue b/src/components/AppNavigation/CalendarList.vue index 7d28adcb4d..1bd7aac092 100644 --- a/src/components/AppNavigation/CalendarList.vue +++ b/src/components/AppNavigation/CalendarList.vue @@ -74,7 +74,7 @@ const delegatedGroups = computed(() => { groups.set(delegatorUrl, { delegatorUrl, displayname: principal?.displayname || principal?.userId || '', - readOnly: !!calendar.readOnly, + readOnly: !calendar.canCreateObject && !calendar.canModifyObject, calendars: [], }) } diff --git a/src/components/AppNavigation/Settings.vue b/src/components/AppNavigation/Settings.vue index f57063991a..1deaf59809 100644 --- a/src/components/AppNavigation/Settings.vue +++ b/src/components/AppNavigation/Settings.vue @@ -359,7 +359,7 @@ export default { defaultCalendarOptions() { return this.calendarsStore.calendars - .filter((calendar) => !calendar.readOnly + .filter((calendar) => (calendar.canCreateObject || calendar.canModifyObject) && !calendar.isSharedWithMe && calendar.supportsEvents) }, diff --git a/src/fullcalendar/eventSources/eventSource.js b/src/fullcalendar/eventSources/eventSource.js index ebca7c1fb7..766e0bfc33 100644 --- a/src/fullcalendar/eventSources/eventSource.js +++ b/src/fullcalendar/eventSources/eventSource.js @@ -59,7 +59,7 @@ export default function() { }, } - if (calendar.readOnly) { + if (!calendar.canCreateObject && !calendar.canModifyObject) { source.editable = false } diff --git a/src/mixins/EditorMixin.js b/src/mixins/EditorMixin.js index 636d787636..bddba5a9b9 100644 --- a/src/mixins/EditorMixin.js +++ b/src/mixins/EditorMixin.js @@ -317,10 +317,11 @@ export default { if (!this.calendarObject) { return false } - if (this.isReadOnly) { + if (this.isLoading) { return false } - if (this.isLoading) { + const calendar = this.calendarsStore.getCalendarById(this.calendarObject.calendarId) + if (!calendar?.canDeleteObject) { return false } diff --git a/src/models/calendar.js b/src/models/calendar.js index 35748377bf..3cc166fdbc 100644 --- a/src/models/calendar.js +++ b/src/models/calendar.js @@ -39,8 +39,6 @@ function getDefaultCalendarObject(props = {}) { publishURL: null, // Internal CalDAV url of this calendar url: '', - // Whether this calendar is read-only - readOnly: false, // The order of this calendar in the calendar-list order: 0, // Whether or not the calendar is shared with me @@ -99,7 +97,6 @@ function mapDavCollectionToCalendar(calendar, currentUserPrincipal) { const supportsJournals = calendar.components.includes('VJOURNAL') const supportsTasks = calendar.components.includes('VTODO') const owner = calendar.owner - const readOnly = !calendar.isWriteable() const canBeShared = calendar.isShareable() const canBePublished = calendar.isPublishable() const canCreateObject = calendar.currentUserPrivilegeSet.includes('{DAV:}bind') || calendar.currentUserPrivilegeSet.includes('{DAV:}write') || calendar.currentUserPrivilegeSet.includes('{DAV:}all') === true @@ -160,7 +157,6 @@ function mapDavCollectionToCalendar(calendar, currentUserPrincipal) { supportsTasks, isSharedWithMe, owner, - readOnly, publishURL, canBeShared, canBePublished, diff --git a/src/store/calendars.js b/src/store/calendars.js index 278efbe124..2ca675f060 100644 --- a/src/store/calendars.js +++ b/src/store/calendars.js @@ -74,7 +74,7 @@ export default defineStore('calendars', { sortedCalendars(state) { return state.calendars .filter((calendar) => calendar.supportsEvents) - .filter((calendar) => !calendar.readOnly) + .filter((calendar) => calendar.canCreateObject || calendar.canModifyObject) .sort((a, b) => a.order - b.order) }, @@ -101,7 +101,7 @@ export default defineStore('calendars', { */ sortedWritableCalendarsEvenWithoutSupportForEvents(state) { return state.calendars - .filter((calendar) => !calendar.readOnly) + .filter((calendar) => calendar.canCreateObject || calendar.canModifyObject) .sort((a, b) => a.order - b.order) }, @@ -114,7 +114,7 @@ export default defineStore('calendars', { ownSortedCalendars(state) { return state.calendars .filter((calendar) => calendar.supportsEvents) - .filter((calendar) => !calendar.readOnly) + .filter((calendar) => calendar.canCreateObject || calendar.canModifyObject) .filter((calendar) => !calendar.isSharedWithMe) .sort((a, b) => a.order - b.order) }, @@ -186,7 +186,7 @@ export default defineStore('calendars', { sortedSubscriptions(state) { return state.calendars .filter((calendar) => calendar.supportsEvents) - .filter((calendar) => calendar.readOnly) + .filter((calendar) => !(calendar.canCreateObject || calendar.canModifyObject)) .sort((a, b) => a.order - b.order) }, diff --git a/src/store/delegation.ts b/src/store/delegation.ts index d538090499..7441f7a5a6 100644 --- a/src/store/delegation.ts +++ b/src/store/delegation.ts @@ -168,8 +168,9 @@ export default defineStore('delegation', () => { * The calendars are tagged with isDelegated=true so CalendarList can show them * in their own section. * - * Read-only delegators' calendars are additionally marked readOnly=true so they - * are excluded from the calendar picker (which only lists writable calendars). + * Read-only delegators' calendars additionally have their write permissions + * cleared so they are excluded from the calendar picker (which only lists + * calendars with canCreateObject or canModifyObject). */ async function fetchDelegatedCalendars(): Promise { if (!delegators.value.length) { @@ -200,7 +201,7 @@ export default defineStore('delegation', () => { isDelegated: true, delegatorUrl: canonicalDelegatorUrl, // Read-only proxy access: prevent editing and hide from calendar picker - ...(permission === 'read' ? { readOnly: true } : {}), + ...(permission === 'read' ? { canCreateObject: false, canModifyObject: false, canDeleteObject: false } : {}), })) for (const calendar of mappedCalendars) { diff --git a/src/types/calendar.ts b/src/types/calendar.ts index 1131c7e793..a1963480dc 100644 --- a/src/types/calendar.ts +++ b/src/types/calendar.ts @@ -66,8 +66,6 @@ export interface CalendarInterface { publishURL: string | null /** Internal CalDAV url of this calendar */ url: string - /** Whether this calendar is read-only */ - readOnly: boolean /** The order of this calendar in the calendar-list */ order: number /** Whether the calendar is shared with the current user */ diff --git a/src/views/Calendar.vue b/src/views/Calendar.vue index 183f203461..20ddbf3345 100644 --- a/src/views/Calendar.vue +++ b/src/views/Calendar.vue @@ -375,7 +375,7 @@ export default { }) const writeableCalendarIndex = calendars.findIndex((calendar) => { - return !calendar.readOnly + return calendar.canCreateObject || calendar.canModifyObject }) // No writeable calendars? Create a new one! diff --git a/tests/javascript/unit/fullcalendar/eventSources/eventSource.test.js b/tests/javascript/unit/fullcalendar/eventSources/eventSource.test.js index 7052986a91..efa94fc70e 100644 --- a/tests/javascript/unit/fullcalendar/eventSources/eventSource.test.js +++ b/tests/javascript/unit/fullcalendar/eventSources/eventSource.test.js @@ -32,7 +32,8 @@ describe('fullcalendar/eventSource test suite', () => { const calendar = { id: 'calendar-id-123', color: '#ff00ff', - readOnly: false, + canCreateObject: true, + canModifyObject: true, } const eventSourceFunction = eventSource() @@ -52,7 +53,8 @@ describe('fullcalendar/eventSource test suite', () => { const calendar = { id: 'calendar-id-123', color: '#ff00ff', - readOnly: true, + canCreateObject: false, + canModifyObject: false, } const eventSourceFunction = eventSource() @@ -82,7 +84,8 @@ describe('fullcalendar/eventSource test suite', () => { const calendar = { id: 'calendar-id-123', color: '#ff00ff', - readOnly: true, + canCreateObject: false, + canModifyObject: false, } const getTimezoneForId = vi.fn() @@ -151,7 +154,8 @@ describe('fullcalendar/eventSource test suite', () => { const calendar = { id: 'calendar-id-123', color: '#ff00ff', - readOnly: true, + canCreateObject: false, + canModifyObject: false, } const getTimezoneForId = vi.fn() @@ -215,7 +219,8 @@ describe('fullcalendar/eventSource test suite', () => { const calendar = { id: 'calendar-id-123', color: '#ff00ff', - readOnly: true, + canCreateObject: false, + canModifyObject: false, } const getTimezoneForId = vi.fn() @@ -282,7 +287,8 @@ describe('fullcalendar/eventSource test suite', () => { const calendar = { id: 'calendar-id-123', color: '#ff00ff', - readOnly: true, + canCreateObject: false, + canModifyObject: false, } const getTimezoneForId = vi.fn() diff --git a/tests/javascript/unit/models/calendar.test.js b/tests/javascript/unit/models/calendar.test.js index 8faa97a69c..aa4382ebb1 100644 --- a/tests/javascript/unit/models/calendar.test.js +++ b/tests/javascript/unit/models/calendar.test.js @@ -29,7 +29,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { publishURL: null, timezone: null, url: '', - readOnly: false, order: 0, isDelegated: false, isSharedWithMe: false, @@ -67,7 +66,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { publishURL: null, timezone: null, url: '', - readOnly: false, order: 0, isDelegated: false, isSharedWithMe: false, @@ -117,7 +115,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { order: 0, owner: '/remote.php/dav/principals/users/admin/', publishURL: null, - readOnly: false, shares: [], supportsEvents: true, supportsJournals: false, @@ -172,7 +169,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { order: 0, owner: '/remote.php/dav/principals/users/admin/', publishURL: null, - readOnly: false, shares: [], supportsEvents: true, supportsJournals: false, @@ -225,7 +221,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { order: 0, owner: '/remote.php/dav/principals/users/admin/', publishURL: null, - readOnly: false, shares: [], supportsEvents: true, supportsJournals: false, @@ -278,7 +273,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { order: 0, owner: '/remote.php/dav/principals/users/admin/', publishURL: null, - readOnly: false, shares: [], supportsEvents: true, supportsJournals: false, @@ -331,7 +325,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { order: 0, owner: '/remote.php/dav/principals/users/admin/', publishURL: null, - readOnly: false, shares: [], supportsEvents: true, supportsJournals: false, @@ -384,7 +377,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { order: 0, owner: '/remote.php/dav/principals/users/admin/', publishURL: null, - readOnly: false, shares: [], supportsEvents: true, supportsJournals: false, @@ -437,7 +429,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { order: 0, owner: '/remote.php/dav/principals/users/admin/', publishURL: null, - readOnly: false, shares: [], supportsEvents: true, supportsJournals: false, @@ -490,7 +481,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { order: 0, owner: '/remote.php/dav/principals/users/admin/', publishURL: null, - readOnly: false, shares: [], supportsEvents: true, supportsJournals: false, @@ -594,7 +584,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { order: 0, owner: '/remote.php/dav/principals/users/admin/', publishURL: null, - readOnly: false, shares: [ { id: 'share1' }, { id: 'share2' }, @@ -724,7 +713,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { order: 0, owner: '/remote.php/dav/principals/users/admin/', publishURL: null, - readOnly: false, shares: [], supportsEvents: true, supportsJournals: false, @@ -778,7 +766,6 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { order: 0, owner: '/remote.php/dav/principals/users/admin/', publishURL: null, - readOnly: false, shares: [], supportsEvents: true, supportsJournals: false, diff --git a/tests/javascript/unit/store/calendars.test.js b/tests/javascript/unit/store/calendars.test.js index ca35819119..4b74fe2703 100644 --- a/tests/javascript/unit/store/calendars.test.js +++ b/tests/javascript/unit/store/calendars.test.js @@ -17,10 +17,13 @@ describe('store/calendars test suite', () => { order: 2, supportsEvents: false, supportsJournals: true, + canCreateObject: true, + canModifyObject: true, } const calendarReadOnly = { id: '2', - readOnly: true, + canCreateObject: false, + canModifyObject: false, supportsEvents: true, } const calendarOrderFirst = { @@ -28,6 +31,8 @@ describe('store/calendars test suite', () => { order: 1, supportsEvents: true, supportsJournals: false, + canCreateObject: true, + canModifyObject: true, } calendarsStore.addCalendarMutation({ calendar: calendarOrderLast }) calendarsStore.addCalendarMutation({ calendar: calendarReadOnly })