Skip to content

Commit 3831f1a

Browse files
committed
feat: add delegation frontend
Signed-off-by: Hamza <hamzamahjoubi221@gmail.com>
1 parent 9520e0c commit 3831f1a

3 files changed

Lines changed: 376 additions & 0 deletions

File tree

src/components/DelegationModal.vue

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<NcModal v-if="view === 'main'" size="normal" @close="$emit('close')">
8+
<div class="delegation-modal">
9+
<h2>{{ t('mail', 'Delegation') }}</h2>
10+
11+
<div class="delegation-modal__section">
12+
<h3>{{ t('mail', 'Delegates') }}</h3>
13+
<p class="delegation-modal__description">
14+
{{ t('mail', 'Allow users to send, receive, and delete mail on your behalf') }}
15+
</p>
16+
17+
<NcListItem
18+
v-for="user in delegates"
19+
:key="user.userId"
20+
:name="user.userId">
21+
<template #icon>
22+
<NcAvatar
23+
disable-menu
24+
:size="34"
25+
:user="user.userId" />
26+
</template>
27+
<template #extra-actions>
28+
<NcButton
29+
:title="t('mail', 'Revoke access')"
30+
:aria-label="t('mail', 'Revoke access')"
31+
variant="tertiary-no-background"
32+
@click="confirmRevoke(user)">
33+
<template #icon>
34+
<IconClose :size="20" />
35+
</template>
36+
</NcButton>
37+
</template>
38+
</NcListItem>
39+
40+
<NcButton
41+
wide
42+
variant="secondary"
43+
@click="openAddDelegate">
44+
<template #icon>
45+
<IconPlus :size="20" />
46+
</template>
47+
{{ t('mail', 'Add delegate') }}
48+
</NcButton>
49+
</div>
50+
</div>
51+
</NcModal>
52+
53+
<NcDialog
54+
v-else-if="view === 'add'"
55+
:open="view === 'add'"
56+
:name="t('mail', 'Add delegate')"
57+
:buttons="addDelegateButtons"
58+
@closing="closeDialog">
59+
<NcSelectUsers
60+
v-model="selectedUser"
61+
:input-label="t('mail', 'Select a user')"
62+
:options="userSuggestions"
63+
:loading="searchLoading"
64+
:placeholder="t('mail', 'Select a user')"
65+
@search="onSearch" />
66+
<p class="add-delegate-description">
67+
{{ t('mail', 'They will be able to send, receive, and delete mail on your behalf') }}
68+
</p>
69+
</NcDialog>
70+
71+
<NcDialog
72+
v-else-if="view === 'revoke'"
73+
:open="view === 'revoke'"
74+
:name="t('mail', 'Revoke access?')"
75+
:buttons="revokeButtons"
76+
@closing="closeDialog">
77+
<p>
78+
{{ revokeText }}
79+
</p>
80+
</NcDialog>
81+
</template>
82+
83+
<script>
84+
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
85+
import IconCheck from '@mdi/svg/svg/check.svg?raw'
86+
import { getCurrentUser } from '@nextcloud/auth'
87+
import axios from '@nextcloud/axios'
88+
import { showError, showSuccess } from '@nextcloud/dialogs'
89+
import { generateOcsUrl } from '@nextcloud/router'
90+
import { ShareType } from '@nextcloud/sharing'
91+
import { NcAvatar, NcButton, NcDialog, NcListItem, NcModal, NcSelectUsers } from '@nextcloud/vue'
92+
import debounce from 'lodash/fp/debounce.js'
93+
import IconClose from 'vue-material-design-icons/Close.vue'
94+
import IconPlus from 'vue-material-design-icons/Plus.vue'
95+
import logger from '../logger.js'
96+
import { delegate, fetchDelegatedUsers, unDelegate } from '../service/DelegationService.js'
97+
98+
export default {
99+
name: 'DelegationModal',
100+
components: {
101+
NcAvatar,
102+
NcButton,
103+
NcDialog,
104+
NcListItem,
105+
NcModal,
106+
NcSelectUsers,
107+
IconClose,
108+
IconPlus,
109+
},
110+
111+
props: {
112+
account: {
113+
type: Object,
114+
required: true,
115+
},
116+
},
117+
118+
data() {
119+
return {
120+
view: 'main',
121+
delegates: [],
122+
revokeUser: null,
123+
selectedUser: null,
124+
userSuggestions: [],
125+
searchLoading: false,
126+
delegating: false,
127+
}
128+
},
129+
130+
computed: {
131+
addDelegateButtons() {
132+
return [
133+
{
134+
label: t('mail', 'Cancel'),
135+
icon: IconCancel,
136+
disabled: this.delegating,
137+
callback: () => { this.closeDialog() },
138+
},
139+
{
140+
label: t('mail', 'Delegate access'),
141+
type: 'primary',
142+
icon: IconCheck,
143+
disabled: !this.selectedUser || this.delegating,
144+
callback: () => { this.addDelegate() },
145+
},
146+
]
147+
},
148+
149+
revokeButtons() {
150+
return [
151+
{
152+
label: t('mail', 'Cancel'),
153+
icon: IconCancel,
154+
callback: () => { this.closeDialog() },
155+
},
156+
{
157+
label: t('mail', 'Revoke'),
158+
type: 'error',
159+
icon: IconCheck,
160+
callback: () => { this.revokeDelegate() },
161+
},
162+
]
163+
},
164+
165+
revokeText() {
166+
if (!this.revokeUser) {
167+
return ''
168+
}
169+
return t('mail', '{userId} will no longer be able to act on your behalf', { userId: this.revokeUser.userId })
170+
},
171+
},
172+
173+
async mounted() {
174+
await this.fetchDelegates()
175+
},
176+
177+
methods: {
178+
async fetchDelegates() {
179+
try {
180+
this.delegates = await fetchDelegatedUsers(this.account.accountId)
181+
} catch (error) {
182+
logger.error('Could not fetch delegates', { error })
183+
showError(t('mail', 'Could not fetch delegates'))
184+
}
185+
},
186+
187+
openAddDelegate() {
188+
this.view = 'add'
189+
},
190+
191+
confirmRevoke(user) {
192+
this.revokeUser = user
193+
this.view = 'revoke'
194+
},
195+
196+
onSearch(query) {
197+
this.debounceGetSuggestions(query.trim())
198+
},
199+
200+
debounceGetSuggestions: debounce(300, function(...args) {
201+
this.getSuggestions(...args)
202+
}),
203+
204+
async getSuggestions(search) {
205+
if (!search) {
206+
this.userSuggestions = []
207+
return
208+
}
209+
210+
this.searchLoading = true
211+
try {
212+
const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1/sharees'), {
213+
params: {
214+
format: 'json',
215+
itemType: 'file',
216+
search,
217+
shareTypes: [ShareType.User],
218+
},
219+
})
220+
221+
const data = request.data.ocs.data
222+
const exact = request.data.ocs.data.exact
223+
224+
const rawSuggestions = exact.users.concat(data.users)
225+
const currentUserId = getCurrentUser().uid
226+
const delegateIds = this.delegates.map((d) => d.userId)
227+
228+
this.userSuggestions = rawSuggestions
229+
.map((result) => ({
230+
id: result.value.shareWith,
231+
displayName: result.name || result.label,
232+
subname: result.value.shareWith,
233+
user: result.value.shareWith,
234+
}))
235+
.filter((u) => u.id !== currentUserId && !delegateIds.includes(u.id))
236+
} catch (error) {
237+
logger.error('Error fetching user suggestions', { error })
238+
} finally {
239+
this.searchLoading = false
240+
}
241+
},
242+
243+
async addDelegate() {
244+
if (!this.selectedUser) {
245+
return
246+
}
247+
248+
this.delegating = true
249+
try {
250+
const delegation = await delegate(this.account.accountId, this.selectedUser.id)
251+
this.delegates.push(delegation)
252+
showSuccess(t('mail', 'Delegated access to {userId}', { userId: this.selectedUser.id }))
253+
this.delegating = false
254+
} catch (error) {
255+
logger.error('Could not delegate access', { error })
256+
showError(t('mail', 'Could not delegate access'))
257+
this.delegating = false
258+
}
259+
},
260+
261+
async revokeDelegate() {
262+
if (!this.revokeUser) {
263+
return
264+
}
265+
266+
try {
267+
await unDelegate(this.account.accountId, this.revokeUser.userId)
268+
this.delegates = this.delegates.filter((d) => d.userId !== this.revokeUser.userId)
269+
showSuccess(t('mail', 'Revoked access for {userId}', { userId: this.revokeUser.userId }))
270+
} catch (error) {
271+
logger.error('Could not revoke delegation', { error })
272+
showError(t('mail', 'Could not revoke delegation'))
273+
}
274+
},
275+
276+
closeDialog() {
277+
this.view = 'main'
278+
},
279+
},
280+
}
281+
</script>
282+
283+
<style lang="scss" scoped>
284+
.delegation-modal {
285+
padding: calc(var(--default-grid-baseline) * 5);
286+
287+
&__section {
288+
margin-top: calc(var(--default-grid-baseline) * 3);
289+
290+
h3 {
291+
font-weight: bold;
292+
}
293+
}
294+
295+
&__description {
296+
color: var(--color-text-maxcontrast);
297+
margin-bottom: calc(var(--default-grid-baseline) * 2);
298+
}
299+
300+
.add-delegate-description {
301+
color: var(--color-text-maxcontrast);
302+
margin-top: calc(var(--default-grid-baseline) * 2);
303+
}
304+
}
305+
</style>

src/components/NavigationAccount.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@
3636
</template>
3737
{{ t('mail', 'Account settings') }}
3838
</ActionButton>
39+
<ActionButton
40+
:close-after-click="true"
41+
@click="showDelegationModal = true">
42+
<template #icon>
43+
<IconDelegation :size="20" />
44+
</template>
45+
{{ t('mail', 'Delegation') }}
46+
</ActionButton>
3947
<ActionCheckbox
4048
:checked="account.showSubscribedOnly"
4149
:disabled="savingShowOnlySubscribed"
@@ -85,6 +93,7 @@
8593
</template>
8694
</NcAppNavigationCaption>
8795
<AccountSettings :open="showSettings" :account="account" @update:open="showAccountSettings($event)" />
96+
<DelegationModal v-if="showDelegationModal" :account="account" @close="showDelegationModal = false" />
8897
</Fragment>
8998
</template>
9099

@@ -95,6 +104,7 @@ import { generateUrl } from '@nextcloud/router'
95104
import { NcActionButton as ActionButton, NcActionCheckbox as ActionCheckbox, NcActionInput as ActionInput, NcActionText as ActionText, NcLoadingIcon as IconLoading, NcAppNavigationCaption } from '@nextcloud/vue'
96105
import { mapStores } from 'pinia'
97106
import { Fragment } from 'vue-frag'
107+
import IconDelegation from 'vue-material-design-icons/AccountMultipleOutline.vue'
98108
import MenuDown from 'vue-material-design-icons/ChevronDown.vue'
99109
import MenuUp from 'vue-material-design-icons/ChevronUp.vue'
100110
import IconSettings from 'vue-material-design-icons/CogOutline.vue'
@@ -115,8 +125,10 @@ export default {
115125
ActionInput,
116126
ActionText,
117127
AccountSettings: () => import(/* webpackChunkName: "account-settings" */ './AccountSettings.vue'),
128+
DelegationModal: () => import(/* webpackChunkName: "delegation-modal" */ './DelegationModal.vue'),
118129
IconInfo,
119130
IconSettings,
131+
IconDelegation,
120132
IconFolderAdd,
121133
MenuDown,
122134
MenuUp,
@@ -162,6 +174,7 @@ export default {
162174
quota: undefined,
163175
editing: false,
164176
showSaving: false,
177+
showDelegationModal: false,
165178
createMailboxName: '',
166179
showMailboxes: false,
167180
nameInput: false,

0 commit comments

Comments
 (0)