Skip to content

Commit 330d4e9

Browse files
authored
Merge pull request #8085 from nextcloud/feat/add-calendar-delegation-features
Feat/add calendar delegation features
2 parents c431c12 + f4ac643 commit 330d4e9

14 files changed

Lines changed: 1073 additions & 16 deletions

File tree

src/components/AppNavigation/CalendarList.vue

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,19 @@
101101
:calendar="calendar" />
102102
</template>
103103

104+
<template v-if="!isPublic && isDelegationSupported && sortedCalendars.delegated.length">
105+
<template v-for="group in delegatedGroups" :key="group.delegatorUrl">
106+
<NcAppNavigationCaption
107+
:name="group.readOnly
108+
? $t('calendar', 'Delegated by {name} (read-only)', { name: group.displayname })
109+
: $t('calendar', 'Delegated by {name}', { name: group.displayname })" />
110+
<CalendarListItem
111+
v-for="calendar in group.calendars"
112+
:key="calendar.id"
113+
:calendar="calendar" />
114+
</template>
115+
</template>
116+
104117
<NcAppNavigationSpacer />
105118

106119
<!-- The header slot must be placed here, otherwise vuedraggable adds undefined as item to the array -->
@@ -122,6 +135,9 @@ import CalendarListItemLoadingPlaceholder from './CalendarList/CalendarListItemL
122135
import CalendarListNew from './CalendarList/CalendarListNew.vue'
123136
import PublicCalendarListItem from './CalendarList/PublicCalendarListItem.vue'
124137
import useCalendarsStore from '../../store/calendars.js'
138+
import useDelegationStore from '../../store/delegation.ts'
139+
import usePrincipalsStore from '../../store/principals.js'
140+
import { isAfterVersion } from '../../utils/nextcloudVersion.ts'
125141
126142
const limit = pLimit(1)
127143
@@ -153,13 +169,14 @@ export default {
153169
return {
154170
calendars: [],
155171
/**
156-
* Calendars sorted by personal, shared, deck, and tasks
172+
* Calendars sorted by personal, shared, deck, and delegated
157173
*/
158174
sortedCalendars: {
159175
personal: [],
160176
shared: [],
161177
deck: [],
162178
tasks: [],
179+
delegated: [],
163180
},
164181
165182
disableDragging: false,
@@ -168,14 +185,43 @@ export default {
168185
},
169186
170187
computed: {
171-
...mapStores(useCalendarsStore),
188+
...mapStores(useCalendarsStore, useDelegationStore, usePrincipalsStore),
172189
...mapState(useCalendarsStore, {
173190
serverCalendars: 'sortedCalendarsSubscriptions',
174191
}),
175192
176193
loadingKeyCalendars() {
177194
return this._uid + '-loading-placeholder-calendars'
178195
},
196+
197+
isDelegationSupported() {
198+
return isAfterVersion(34)
199+
},
200+
201+
/**
202+
* Delegated calendars grouped by the delegator (the user who granted
203+
* proxy access), which may differ from each calendar's owner when the
204+
* delegator only has access via a regular share.
205+
*
206+
* @return {Array<{delegatorUrl: string, displayname: string, calendars: object[]}>}
207+
*/
208+
delegatedGroups() {
209+
const groups = new Map()
210+
for (const calendar of this.sortedCalendars.delegated) {
211+
const delegatorUrl = calendar.delegatorUrl || calendar.owner || ''
212+
if (!groups.has(delegatorUrl)) {
213+
const principal = this.principalsStore.getPrincipalByUrl(delegatorUrl)
214+
groups.set(delegatorUrl, {
215+
delegatorUrl,
216+
displayname: principal?.displayname || principal?.userId || '',
217+
readOnly: !!calendar.readOnly,
218+
calendars: [],
219+
})
220+
}
221+
groups.get(delegatorUrl).calendars.push(calendar)
222+
}
223+
return Array.from(groups.values())
224+
},
179225
},
180226
181227
watch: {
@@ -208,9 +254,15 @@ export default {
208254
shared: [],
209255
deck: [],
210256
tasks: [],
257+
delegated: [],
211258
}
212259
213260
this.calendars.forEach((calendar) => {
261+
if (calendar.isDelegated) {
262+
this.sortedCalendars.delegated.push(calendar)
263+
return
264+
}
265+
214266
if (calendar.isSharedWithMe) {
215267
this.sortedCalendars.shared.push(calendar)
216268
return

src/components/AppNavigation/CalendarList/CalendarListItem.vue

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,36 @@
2626

2727
<template #counter>
2828
<LinkVariant v-if="isSharedByMe" :size="20" />
29+
<NcAvatar
30+
v-else-if="isDelegated && loadedDelegatorPrincipal && !actionsMenuOpen"
31+
:user="delegatorUserId"
32+
:displayName="delegatorDisplayname"
33+
:title="delegatorDisplayname"
34+
:hideStatus="true"
35+
:size="20"
36+
class="delegated-counter-avatar" />
2937
<NcAvatar
3038
v-else-if="isSharedWithMe && loadedOwnerPrincipal && !actionsMenuOpen"
3139
:user="ownerUserId"
3240
:displayName="ownerDisplayname" />
33-
<div v-else-if="isSharedWithMe && !loadedOwnerPrincipal" class="icon icon-loading" />
41+
<div v-else-if="(isSharedWithMe && !loadedOwnerPrincipal) || (isDelegated && !loadedDelegatorPrincipal)" class="icon icon-loading" />
3442
</template>
3543

3644
<template #actions>
3745
<template v-if="!isBeingDeleted">
38-
<template v-if="isSharedWithMe">
46+
<template v-if="isDelegated">
47+
<NcActionCaption :name="$t('calendar', 'Delegated to you by')" />
48+
<NcActionText class="delegated-action-text">
49+
<template #icon>
50+
<div class="actions-icon-avatar">
51+
<NcAvatar :user="delegatorUserId" :displayName="delegatorDisplayname" :size="30" />
52+
</div>
53+
</template>
54+
{{ delegatorDisplayname }}
55+
</NcActionText>
56+
<NcActionSeparator />
57+
</template>
58+
<template v-else-if="isSharedWithMe">
3959
<NcActionCaption :name="$t('calendar', 'Shared with you by')" />
4060
<NcActionText>
4161
<template #icon>
@@ -143,7 +163,7 @@ export default {
143163
canBeShared() {
144164
// The backend falsely reports incoming editable shares as being shareable
145165
// Ref https://github.com/nextcloud/calendar/issues/5755
146-
if (this.calendar.isSharedWithMe) {
166+
if (this.calendar.isSharedWithMe || this.calendar.isDelegated) {
147167
return false
148168
}
149169
@@ -165,7 +185,16 @@ export default {
165185
* @return {boolean}
166186
*/
167187
isSharedWithMe() {
168-
return this.calendar.isSharedWithMe
188+
return this.calendar.isSharedWithMe && !this.calendar.isDelegated
189+
},
190+
191+
/**
192+
* Is the calendar delegated to me by another user?
193+
*
194+
* @return {boolean}
195+
*/
196+
isDelegated() {
197+
return !!this.calendar.isDelegated
169198
},
170199
171200
/**
@@ -186,6 +215,10 @@ export default {
186215
return this.principalsStore.getPrincipalByUrl(this.calendar.owner) !== undefined
187216
},
188217
218+
loadedDelegatorPrincipal() {
219+
return this.principalsStore.getPrincipalByUrl(this.calendar.delegatorUrl) !== undefined
220+
},
221+
189222
ownerUserId() {
190223
const principal = this.principalsStore.getPrincipalByUrl(this.calendar.owner)
191224
if (principal) {
@@ -204,6 +237,16 @@ export default {
204237
return ''
205238
},
206239
240+
delegatorUserId() {
241+
const principal = this.principalsStore.getPrincipalByUrl(this.calendar.delegatorUrl)
242+
return principal?.userId || ''
243+
},
244+
245+
delegatorDisplayname() {
246+
const principal = this.principalsStore.getPrincipalByUrl(this.calendar.delegatorUrl)
247+
return principal?.displayname || principal?.userId || ''
248+
},
249+
207250
/**
208251
* compute aria-description for AppNavigationItem link
209252
*
@@ -308,6 +351,16 @@ export default {
308351
height: 44px;
309352
}
310353
354+
// Size and position the delegated avatar in the counter slot to match icon buttons
355+
.delegated-counter-avatar {
356+
margin-inline-start: auto;
357+
}
358+
359+
// Vertically align the owner name with the avatar in the "Delegated to you by" row
360+
:deep(.action-text__text) {
361+
align-self: center ;
362+
}
363+
311364
// Hide avatars if list item is hovered
312365
:deep(.app-navigation-entry:hover .app-navigation-entry__counter-wrapper) {
313366
display: none;

src/components/AppNavigation/Settings.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@
135135
<SettingsAttachmentsFolder />
136136
</NcFormGroup>
137137
</NcAppSettingsSection>
138+
<NcAppSettingsSection
139+
v-if="isDelegationSupported"
140+
id="settings-modal-delegation"
141+
:name="t('calendar', 'Delegation')">
142+
<SettingsDelegationSection />
143+
</NcAppSettingsSection>
138144
<EventLegend />
139145
<ShortcutOverview />
140146
</NcAppSettingsDialog>
@@ -167,6 +173,7 @@ import CogIcon from 'vue-material-design-icons/CogOutline.vue'
167173
import CalendarPicker from '../Shared/CalendarPicker.vue'
168174
import EventLegend from './Settings/EventLegend.vue'
169175
import SettingsAttachmentsFolder from './Settings/SettingsAttachmentsFolder.vue'
176+
import SettingsDelegationSection from './Settings/SettingsDelegationSection.vue'
170177
import SettingsImportSection from './Settings/SettingsImportSection.vue'
171178
import SettingsTimezoneSelect from './Settings/SettingsTimezoneSelect.vue'
172179
import ShortcutOverview from './Settings/ShortcutOverview.vue'
@@ -187,6 +194,7 @@ import {
187194
getAmountHoursMinutesAndUnitForAllDayEvents,
188195
} from '../../utils/alarms.js'
189196
import logger from '../../utils/logger.js'
197+
import { isAfterVersion } from '../../utils/nextcloudVersion.ts'
190198
191199
export default {
192200
name: 'Settings',
@@ -199,6 +207,7 @@ export default {
199207
SettingsImportSection,
200208
SettingsTimezoneSelect,
201209
SettingsAttachmentsFolder,
210+
SettingsDelegationSection,
202211
ShortcutOverview,
203212
CogIcon,
204213
NcFormBox,
@@ -264,6 +273,10 @@ export default {
264273
return this.savingBirthdayCalendar || this.loadingCalendars
265274
},
266275
276+
isDelegationSupported() {
277+
return isAfterVersion(34)
278+
},
279+
267280
files() {
268281
return this.importFilesStore.importFiles
269282
},

0 commit comments

Comments
 (0)