diff --git a/src/components/AppNavigation/CalendarList.vue b/src/components/AppNavigation/CalendarList.vue index 7e32e374dc..ed37317380 100644 --- a/src/components/AppNavigation/CalendarList.vue +++ b/src/components/AppNavigation/CalendarList.vue @@ -101,6 +101,19 @@ :calendar="calendar" /> + + + + + + + @@ -122,6 +135,9 @@ import CalendarListItemLoadingPlaceholder from './CalendarList/CalendarListItemL import CalendarListNew from './CalendarList/CalendarListNew.vue' import PublicCalendarListItem from './CalendarList/PublicCalendarListItem.vue' import useCalendarsStore from '../../store/calendars.js' +import useDelegationStore from '../../store/delegation.ts' +import usePrincipalsStore from '../../store/principals.js' +import { isAfterVersion } from '../../utils/nextcloudVersion.ts' const limit = pLimit(1) @@ -153,13 +169,14 @@ export default { return { calendars: [], /** - * Calendars sorted by personal, shared, deck, and tasks + * Calendars sorted by personal, shared, deck, and delegated */ sortedCalendars: { personal: [], shared: [], deck: [], tasks: [], + delegated: [], }, disableDragging: false, @@ -168,7 +185,7 @@ export default { }, computed: { - ...mapStores(useCalendarsStore), + ...mapStores(useCalendarsStore, useDelegationStore, usePrincipalsStore), ...mapState(useCalendarsStore, { serverCalendars: 'sortedCalendarsSubscriptions', }), @@ -176,6 +193,35 @@ export default { loadingKeyCalendars() { return this._uid + '-loading-placeholder-calendars' }, + + isDelegationSupported() { + return isAfterVersion(34) + }, + + /** + * Delegated calendars grouped by the delegator (the user who granted + * proxy access), which may differ from each calendar's owner when the + * delegator only has access via a regular share. + * + * @return {Array<{delegatorUrl: string, displayname: string, calendars: object[]}>} + */ + delegatedGroups() { + const groups = new Map() + for (const calendar of this.sortedCalendars.delegated) { + const delegatorUrl = calendar.delegatorUrl || calendar.owner || '' + if (!groups.has(delegatorUrl)) { + const principal = this.principalsStore.getPrincipalByUrl(delegatorUrl) + groups.set(delegatorUrl, { + delegatorUrl, + displayname: principal?.displayname || principal?.userId || '', + readOnly: !!calendar.readOnly, + calendars: [], + }) + } + groups.get(delegatorUrl).calendars.push(calendar) + } + return Array.from(groups.values()) + }, }, watch: { @@ -208,9 +254,15 @@ export default { shared: [], deck: [], tasks: [], + delegated: [], } this.calendars.forEach((calendar) => { + if (calendar.isDelegated) { + this.sortedCalendars.delegated.push(calendar) + return + } + if (calendar.isSharedWithMe) { this.sortedCalendars.shared.push(calendar) return diff --git a/src/components/AppNavigation/CalendarList/CalendarListItem.vue b/src/components/AppNavigation/CalendarList/CalendarListItem.vue index b8cc504872..c2501a76ea 100644 --- a/src/components/AppNavigation/CalendarList/CalendarListItem.vue +++ b/src/components/AppNavigation/CalendarList/CalendarListItem.vue @@ -26,16 +26,36 @@ + - + - + + + + + + + + + {{ delegatorDisplayname }} + + + + @@ -143,7 +163,7 @@ export default { canBeShared() { // The backend falsely reports incoming editable shares as being shareable // Ref https://github.com/nextcloud/calendar/issues/5755 - if (this.calendar.isSharedWithMe) { + if (this.calendar.isSharedWithMe || this.calendar.isDelegated) { return false } @@ -165,7 +185,16 @@ export default { * @return {boolean} */ isSharedWithMe() { - return this.calendar.isSharedWithMe + return this.calendar.isSharedWithMe && !this.calendar.isDelegated + }, + + /** + * Is the calendar delegated to me by another user? + * + * @return {boolean} + */ + isDelegated() { + return !!this.calendar.isDelegated }, /** @@ -186,6 +215,10 @@ export default { return this.principalsStore.getPrincipalByUrl(this.calendar.owner) !== undefined }, + loadedDelegatorPrincipal() { + return this.principalsStore.getPrincipalByUrl(this.calendar.delegatorUrl) !== undefined + }, + ownerUserId() { const principal = this.principalsStore.getPrincipalByUrl(this.calendar.owner) if (principal) { @@ -204,6 +237,16 @@ export default { return '' }, + delegatorUserId() { + const principal = this.principalsStore.getPrincipalByUrl(this.calendar.delegatorUrl) + return principal?.userId || '' + }, + + delegatorDisplayname() { + const principal = this.principalsStore.getPrincipalByUrl(this.calendar.delegatorUrl) + return principal?.displayname || principal?.userId || '' + }, + /** * compute aria-description for AppNavigationItem link * @@ -308,6 +351,16 @@ export default { height: 44px; } + // Size and position the delegated avatar in the counter slot to match icon buttons + .delegated-counter-avatar { + margin-inline-start: auto; + } + + // Vertically align the owner name with the avatar in the "Delegated to you by" row + :deep(.action-text__text) { + align-self: center ; + } + // Hide avatars if list item is hovered :deep(.app-navigation-entry:hover .app-navigation-entry__counter-wrapper) { display: none; diff --git a/src/components/AppNavigation/Settings.vue b/src/components/AppNavigation/Settings.vue index e71b95ccf8..a70a5a5413 100644 --- a/src/components/AppNavigation/Settings.vue +++ b/src/components/AppNavigation/Settings.vue @@ -135,6 +135,12 @@ + + + @@ -167,6 +173,7 @@ import CogIcon from 'vue-material-design-icons/CogOutline.vue' import CalendarPicker from '../Shared/CalendarPicker.vue' import EventLegend from './Settings/EventLegend.vue' import SettingsAttachmentsFolder from './Settings/SettingsAttachmentsFolder.vue' +import SettingsDelegationSection from './Settings/SettingsDelegationSection.vue' import SettingsImportSection from './Settings/SettingsImportSection.vue' import SettingsTimezoneSelect from './Settings/SettingsTimezoneSelect.vue' import ShortcutOverview from './Settings/ShortcutOverview.vue' @@ -187,6 +194,7 @@ import { getAmountHoursMinutesAndUnitForAllDayEvents, } from '../../utils/alarms.js' import logger from '../../utils/logger.js' +import { isAfterVersion } from '../../utils/nextcloudVersion.ts' export default { name: 'Settings', @@ -199,6 +207,7 @@ export default { SettingsImportSection, SettingsTimezoneSelect, SettingsAttachmentsFolder, + SettingsDelegationSection, ShortcutOverview, CogIcon, NcFormBox, @@ -264,6 +273,10 @@ export default { return this.savingBirthdayCalendar || this.loadingCalendars }, + isDelegationSupported() { + return isAfterVersion(34) + }, + files() { return this.importFilesStore.importFiles }, diff --git a/src/components/AppNavigation/Settings/SettingsDelegationSection.vue b/src/components/AppNavigation/Settings/SettingsDelegationSection.vue new file mode 100644 index 0000000000..11c74a04ae --- /dev/null +++ b/src/components/AppNavigation/Settings/SettingsDelegationSection.vue @@ -0,0 +1,440 @@ + + + + + + {{ t('calendar', 'Could not load delegates.') }} + + + + + + + + {{ delegateSubname(delegate) }} + + + + + + + {{ t('calendar', 'Revoke access') }} + + + + + + + {{ t('calendar', 'No delegates yet.') }} + + + + + + + {{ t('calendar', 'Add delegate') }} + + + + + + + + + + + + {{ t('calendar', 'No users found') }} + + + + + + {{ user.displayname }} + {{ user.emailAddress }} + + + + + + {{ t('calendar', 'Choose whether they can view and edit events, or only view them.') }} + + + + + {{ t('calendar', 'Cancel') }} + + + {{ t('calendar', 'Add as viewer') }} + + + {{ t('calendar', 'Add as editor') }} + + + + + + + + + diff --git a/src/components/AppointmentConfigModal/DurationInput.vue b/src/components/AppointmentConfigModal/DurationInput.vue index 59a528f890..8e0e8af3a7 100644 --- a/src/components/AppointmentConfigModal/DurationInput.vue +++ b/src/components/AppointmentConfigModal/DurationInput.vue @@ -85,7 +85,7 @@ export default { } // Emit value in seconds - // eslint-disable-next-line vue/require-explicit-emits + this.$emit('update:modelValue', this.parsedInternalValue * 60) }, diff --git a/src/components/Editor/CalendarPickerHeader.vue b/src/components/Editor/CalendarPickerHeader.vue index 494287d10d..99e76a7e47 100644 --- a/src/components/Editor/CalendarPickerHeader.vue +++ b/src/components/Editor/CalendarPickerHeader.vue @@ -15,7 +15,7 @@ :class="{ 'calendar-picker-header__picker--has-menu': !isReadOnly && calendars.length > 1, }" - :menuName="value.displayName" + :menuName="getOptionLabel(value)" :forceName="true" :disabled="isDisabled"> @@ -34,26 +34,68 @@ @click="$emit('update:value', calendar)"> + + - {{ calendar.displayName }} + {{ getOptionLabel(calendar) }} + + @@ -142,6 +297,10 @@ export default { :deep(button) { align-items: center !important; } + + &__avatar { + margin: auto; + } } // Fix long calendar name ellipsis @@ -162,6 +321,12 @@ export default { } } + &__avatar { + flex-shrink: 0; + align-self: center; + margin-inline-start: var(--default-grid-baseline); + } + &__icon { display: flex; align-items: center; diff --git a/src/components/Shared/CalendarPicker.vue b/src/components/Shared/CalendarPicker.vue index 91faa0ceea..2c428ccd78 100644 --- a/src/components/Shared/CalendarPicker.vue +++ b/src/components/Shared/CalendarPicker.vue @@ -21,6 +21,8 @@ :color="getCalendarById(id).color" :displayName="getCalendarById(id).displayName" :isSharedWithMe="getCalendarById(id).isSharedWithMe" + :isDelegated="getCalendarById(id).isDelegated" + :delegatorUrl="getCalendarById(id).delegatorUrl" :owner="getCalendarById(id).owner" /> @@ -28,6 +30,8 @@ :color="getCalendarById(id).color" :displayName="getCalendarById(id).displayName" :isSharedWithMe="getCalendarById(id).isSharedWithMe" + :isDelegated="getCalendarById(id).isDelegated" + :delegatorUrl="getCalendarById(id).delegatorUrl" :owner="getCalendarById(id).owner" /> diff --git a/src/components/Shared/CalendarPickerOption.vue b/src/components/Shared/CalendarPickerOption.vue index 1c528f6fe8..f5123579ab 100644 --- a/src/components/Shared/CalendarPickerOption.vue +++ b/src/components/Shared/CalendarPickerOption.vue @@ -11,10 +11,21 @@ {{ displayName }} + + {{ $t('calendar', '(delegated by {name})', { name: delegatorDisplayName }) }} + + + + diff --git a/src/models/calendar.js b/src/models/calendar.js index 8b478bf474..3617bc502f 100644 --- a/src/models/calendar.js +++ b/src/models/calendar.js @@ -45,6 +45,11 @@ function getDefaultCalendarObject(props = {}) { order: 0, // Whether or not the calendar is shared with me isSharedWithMe: false, + // Whether or not the calendar belongs to a user who delegated to me + isDelegated: false, + // Principal URL of the delegator whose home this calendar was fetched from + // (may differ from `owner` when the delegator only has access via a share) + delegatorUrl: '', // Whether or not the calendar can be shared by me canBeShared: false, // Whether or not the calendar can be published by me diff --git a/src/services/caldavService.js b/src/services/caldavService.js index 851cebb78d..a61defe695 100644 --- a/src/services/caldavService.js +++ b/src/services/caldavService.js @@ -250,6 +250,18 @@ async function findPrincipalsInCollection(url, options = {}) { return getClient().findPrincipalsInCollection(url, options) } +/** + * Fetches all calendars from a calendar home at an arbitrary URL. + * Used to load calendars from another user's calendar home when acting as their proxy. + * + * @param {string} calendarHomeUrl Absolute URL of the calendar home to fetch from + * @return {Promise} Raw cdav-library Calendar objects + */ +async function findCalendarsAtUrl(calendarHomeUrl) { + const calendarHome = getClient().getCalendarHomeForUrl(calendarHomeUrl) + return calendarHome.findAllCalendars() +} + export { advancedPrincipalPropertySearch, createCalendar, @@ -258,12 +270,14 @@ export { findAll, findAllCalendars, findAllDeletedCalendars, + findCalendarsAtUrl, findPrincipalByUrl, findPrincipalsInCollection, findPublicCalendarsByTokens, findSchedulingInbox, findSchedulingOutbox, getBirthdayCalendar, + getClient, getCurrentUserPrincipal, initializeClientForPublicView, initializeClientForUserView, diff --git a/src/store/delegation.ts b/src/store/delegation.ts new file mode 100644 index 0000000000..0035378257 --- /dev/null +++ b/src/store/delegation.ts @@ -0,0 +1,231 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { showError } from '@nextcloud/dialogs' +import { translate as t } from '@nextcloud/l10n' +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import useCalendarsStore from './calendars.js' +import usePrincipalsStore from './principals.js' +import { mapDavCollectionToCalendar } from '@/models/calendar.js' +import { mapDavToPrincipal } from '@/models/principal.js' +import { findCalendarsAtUrl, findPrincipalByUrl, getClient } from '@/services/caldavService.js' +import logger from '@/utils/logger.js' + +export interface DelegatePrincipal { + id: string | null + calendarUserType: string + emailAddress: string | null + displayname: string | null + principalScheme: string | null + userId: string | null + url: string | null + dav: unknown + isCircle: boolean + isUser: boolean + isGroup: boolean + isCalendarResource: boolean + isCalendarRoom: boolean + principalId: string | null + scheduleDefaultCalendarUrl: string | null + permission: 'write' | 'read' +} + +export interface Delegator { + principalUrl: string + permission: 'write' | 'read' +} + +export default defineStore('delegation', () => { + /** + * List of principal objects that the current user has delegated to. + * Each entry has the principal fields plus a `permission` field ('write'|'read'). + */ + const delegates = ref([]) + + /** + * Users who have granted the current user proxy access, with permission level. + */ + const delegators = ref([]) + + /** + * Whether any delegated calendars exist (i.e. the current user is a delegate for someone). + */ + const hasDelegatedCalendars = computed(() => delegators.value.length > 0) + + /** + * Fetch the current user's delegates (members of their calendar-proxy-write + * and calendar-proxy-read groups) and resolve their principal details. + */ + async function fetchDelegates(): Promise { + const principalsStore = usePrincipalsStore() + const currentUser = principalsStore.getCurrentUserPrincipal + if (!currentUser?.url) { + return + } + + let writeUrls: string[] = [] + let readUrls: string[] = [] + try { + const delegateUrls = await getClient().getDelegatesForPrincipal(currentUser.url) + writeUrls = delegateUrls.write + readUrls = delegateUrls.read + } catch (error) { + logger.error('Could not fetch delegate URLs', { error }) + return + } + + const result: DelegatePrincipal[] = [] + + for (const url of writeUrls) { + try { + const dav = await findPrincipalByUrl(url) + if (dav) { + result.push({ ...mapDavToPrincipal(dav), permission: 'write' }) + } + } catch (error) { + logger.error('Could not resolve write delegate principal', { url, error }) + } + } + + for (const url of readUrls) { + try { + const dav = await findPrincipalByUrl(url) + if (dav) { + result.push({ ...mapDavToPrincipal(dav), permission: 'read' }) + } + } catch (error) { + logger.error('Could not resolve read delegate principal', { url, error }) + } + } + + delegates.value = result + logger.debug('Fetched delegates', { delegates: delegates.value }) + } + + /** + * Fetch the principal URLs and permission level of users who have granted + * the current user proxy access (both read and write). + */ + async function fetchDelegators(): Promise { + const principalsStore = usePrincipalsStore() + const currentUser = principalsStore.getCurrentUserPrincipal + if (!currentUser?.url) { + return + } + + try { + delegators.value = await getClient().getDelegatorsWithPermission(currentUser.url) + logger.debug('Fetched delegators', { delegators: delegators.value }) + } catch (error) { + logger.error('Could not fetch delegator information', { error }) + } + } + + /** + * Add a user as a delegate with the given permission level. + * + * @param principalUrl - Absolute URL of the principal to add + * @param permission - The permission level to grant + */ + async function addDelegate({ principalUrl, permission = 'write' as 'write' | 'read' }): Promise { + const principalsStore = usePrincipalsStore() + const currentUser = principalsStore.getCurrentUserPrincipal + if (!currentUser?.url) { + return + } + + await getClient().addDelegate(currentUser.url, principalUrl, permission) + await fetchDelegates() + } + + /** + * Remove a delegate from whichever proxy group(s) they are in. + * + * @param principalUrl - Absolute URL of the principal to remove + */ + async function removeDelegate({ principalUrl }: { principalUrl: string }): Promise { + const principalsStore = usePrincipalsStore() + const currentUser = principalsStore.getCurrentUserPrincipal + if (!currentUser?.url) { + return + } + + // Find the delegate's current permission so we remove from the right group. + const existing = delegates.value.find((d) => d.url === principalUrl) + const permission = existing?.permission ?? 'write' + + await getClient().removeDelegate(currentUser.url, principalUrl, permission) + delegates.value = delegates.value.filter((d) => d.url !== principalUrl) + } + + /** + * Fetch all calendars from delegators' calendar homes and add them to the + * calendars store so they participate in normal event fetching and rendering. + * 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). + */ + async function fetchDelegatedCalendars(): Promise { + if (!delegators.value.length) { + return + } + + const principalsStore = usePrincipalsStore() + const calendarsStore = useCalendarsStore() + const currentUser = principalsStore.getCurrentUserPrincipal + + for (const { principalUrl: delegatorPrincipalUrl, permission } of delegators.value) { + const calendarHomeUrl = await getClient().getCalendarHomeUrlForPrincipal(delegatorPrincipalUrl) + if (!calendarHomeUrl) { + logger.warn('Could not determine calendar home URL for delegator', { delegatorPrincipalUrl }) + showError(t('calendar', 'Could not load delegated calendars. Make sure the server supports calendar delegation.')) + continue + } + + try { + const delegatorPrincipal = await principalsStore.fetchPrincipalByUrl({ url: delegatorPrincipalUrl }) + const canonicalDelegatorUrl = delegatorPrincipal?.url || delegatorPrincipalUrl + + const rawCalendars = await findCalendarsAtUrl(calendarHomeUrl) + const mappedCalendars = rawCalendars + .map((cal: unknown) => mapDavCollectionToCalendar(cal, currentUser)) + .map((cal: Record) => ({ + ...cal, + isDelegated: true, + delegatorUrl: canonicalDelegatorUrl, + // Read-only proxy access: prevent editing and hide from calendar picker + ...(permission === 'read' ? { readOnly: true } : {}), + })) + + for (const calendar of mappedCalendars) { + if (!calendarsStore.getCalendarById(calendar.id)) { + calendarsStore.addCalendarMutation({ calendar }) + } + } + + const ownerUrls = [...new Set(mappedCalendars.map((cal: Record) => cal.owner).filter(Boolean))] as string[] + await Promise.all(ownerUrls.map((url) => principalsStore.fetchPrincipalByUrl({ url }))) + + logger.debug('Fetched delegated calendars from', { calendarHomeUrl, permission, count: mappedCalendars.length }) + } catch (error) { + logger.error('Could not fetch calendars for delegator', { delegatorPrincipalUrl, error }) + showError(t('calendar', 'Could not load delegated calendars. Make sure the server supports calendar delegation.')) + } + } + } + + return { + delegates, + delegators, + hasDelegatedCalendars, + fetchDelegates, + fetchDelegators, + addDelegate, + removeDelegate, + fetchDelegatedCalendars, + } +}) diff --git a/src/store/principals.js b/src/store/principals.js index b6506738aa..40e6482e54 100644 --- a/src/store/principals.js +++ b/src/store/principals.js @@ -80,18 +80,20 @@ export default defineStore('principals', { * @return {Promise} */ async fetchPrincipalByUrl({ url }) { - // Don't refetch principals we already have - if (this.getPrincipalByUrl(url)) { - return + const existing = this.getPrincipalByUrl(url) + if (existing) { + return existing } const principal = await findPrincipalByUrl(url) if (!principal) { // TODO - handle error - return + return undefined } - this.addPrincipalMutation({ principal: mapDavToPrincipal(principal) }) + const mapped = mapDavToPrincipal(principal) + this.addPrincipalMutation({ principal: mapped }) + return this.getPrincipalById(mapped.id) }, /** diff --git a/src/views/Calendar.vue b/src/views/Calendar.vue index 078bd11d82..52e7722466 100644 --- a/src/views/Calendar.vue +++ b/src/views/Calendar.vue @@ -134,6 +134,7 @@ import { isNotifyPushAvailable, registerNotifyPushSyncListener } from '../servic import getTimezoneManager from '../services/timezoneDataProviderService.js' import useCalendarObjectsStore from '../store/calendarObjects.js' import useCalendarsStore from '../store/calendars.js' +import useDelegationStore from '../store/delegation.ts' import useFetchedTimeRangesStore from '../store/fetchedTimeRanges.js' import usePrincipalsStore from '../store/principals.js' import useSettingsStore from '../store/settings.js' @@ -147,6 +148,7 @@ import { } from '../utils/date.js' import logger from '../utils/logger.js' import loadMomentLocalization from '../utils/moment.js' +import { isAfterVersion } from '../utils/nextcloudVersion.ts' import '@nextcloud/dialogs/style.css' @@ -220,6 +222,7 @@ export default { usePrincipalsStore, useSettingsStore, useWidgetStore, + useDelegationStore, ), ...mapState(useSettingsStore, { @@ -386,6 +389,12 @@ export default { }) } + // Load delegation info: who has delegated their calendars to the current user + if (isAfterVersion(34)) { + await this.delegationStore.fetchDelegators() + await this.delegationStore.fetchDelegatedCalendars() + } + this.loadingCalendars = false } }, diff --git a/tests/javascript/unit/models/calendar.test.js b/tests/javascript/unit/models/calendar.test.js index 951784053d..20fee7366e 100644 --- a/tests/javascript/unit/models/calendar.test.js +++ b/tests/javascript/unit/models/calendar.test.js @@ -32,6 +32,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { url: '', readOnly: false, order: 0, + isDelegated: false, isSharedWithMe: false, canBeShared: false, canBePublished: false, @@ -44,6 +45,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { transparency: 'opaque', defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) }) @@ -68,6 +70,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { url: '', readOnly: false, order: 0, + isDelegated: false, isSharedWithMe: false, canBeShared: false, canBePublished: false, @@ -80,6 +83,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { transparency: 'opaque', defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) }) @@ -119,6 +123,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -131,6 +136,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { loading: false, defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) @@ -172,6 +178,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -184,6 +191,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { loading: false, defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) @@ -223,6 +231,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -235,6 +244,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { loading: false, defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) @@ -274,6 +284,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: true, canCreateObject: false, canDeleteObject: false, @@ -286,6 +297,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { loading: false, defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) @@ -325,6 +337,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -337,6 +350,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { loading: false, defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) @@ -376,6 +390,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -388,6 +403,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { loading: false, defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) @@ -427,6 +443,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -439,6 +456,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { loading: false, defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) @@ -478,6 +496,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -490,6 +509,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { loading: false, defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) @@ -585,6 +605,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -597,6 +618,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { loading: false, defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(4) @@ -708,6 +730,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: true, canCreateObject: false, canDeleteObject: false, @@ -720,6 +743,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { loading: false, defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) @@ -760,6 +784,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -772,6 +797,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { loading: false, defaultAlarmFullDay: null, defaultAlarmPartDay: null, + delegatorUrl: '', }) })
+ {{ t('calendar', 'No delegates yet.') }} +
+ {{ t('calendar', 'Choose whether they can view and edit events, or only view them.') }} +