Skip to content

Commit d0d6fea

Browse files
committed
feat(files_sharing): use the unified sharing dialog in the files sidebar
Replace the legacy inline sharing inputs and per-share menus with the new unified sharing dialog (@nextcloud/sharing/dialog): - a single "Share" button opens the create dialog - share entries expose dedicated edit + delete actions, delete asking for confirmation before removing the share - the quick-share permission dropdown and the link "customize" entry route through the new dialog The dialog is a Vue 3 component while files_sharing is still a Vue 2 frontend, so it is registered on the global OCA.Sharing namespace by a small Vue 3 bridge entry point (build/frontend) and triggered from the Vue 2 sidebar through that namespace. This keeps Vue 3 out of the legacy bundle; once files_sharing is migrated to Vue 3 the bridge can be dropped and the library imported directly. The legacy inline UI is kept as an automatic fallback, shown whenever the server does not advertise the unified sharing API (empty sharing capability). Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
1 parent a89d13f commit d0d6fea

9 files changed

Lines changed: 253 additions & 4 deletions

File tree

apps/files_sharing/lib/Listener/LoadSidebarListener.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ public function handle(Event $event): void {
3838
return;
3939
}
4040
Util::addScript(Application::APP_ID, 'files_sharing_tab', 'files');
41+
// Vue 3 bridge exposing the unified sharing dialog on OCA.Sharing for the
42+
// (Vue 2) sidebar to trigger without bundling Vue 3.
43+
Util::addScript(Application::APP_ID, 'sharing-dialog', 'files');
4144

4245
$appConfig = Server::get(IAppConfig::class);
4346
$gsConfig = Server::get(IConfig::class);

apps/files_sharing/src/components/SharingEntry.vue

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@
2828
</span>
2929
</component>
3030
<SharingEntryQuickShareSelect
31+
v-if="!config.sharingDialogEnabled"
3132
:share="share"
3233
:file-info="fileInfo"
3334
@open-sharing-details="openShareDetailsForCustomSettings(share)" />
3435
</div>
3536
<ShareExpiryTime v-if="share && share.expireDate" :share="share" />
3637
<NcButton
37-
v-if="share.canEdit"
38+
v-if="share.canEdit && !config.sharingDialogEnabled"
3839
class="sharing-entry__action"
3940
data-cy-files-sharing-share-actions
4041
:aria-label="t('files_sharing', 'Open Sharing Details')"
@@ -44,27 +45,57 @@
4445
<DotsHorizontalIcon :size="20" />
4546
</template>
4647
</NcButton>
48+
<!-- Unified dialog: edit + delete as a simple dual button -->
49+
<template v-else-if="config.sharingDialogEnabled">
50+
<NcButton
51+
v-if="share.canEdit"
52+
class="sharing-entry__action"
53+
:aria-label="t('files_sharing', 'Edit share')"
54+
variant="tertiary"
55+
@click="openEditDialog">
56+
<template #icon>
57+
<PencilIcon :size="20" />
58+
</template>
59+
</NcButton>
60+
<NcButton
61+
v-if="share.canDelete"
62+
class="sharing-entry__action"
63+
:aria-label="t('files_sharing', 'Delete share')"
64+
variant="tertiary"
65+
@click="confirmDelete">
66+
<template #icon>
67+
<DeleteIcon :size="20" />
68+
</template>
69+
</NcButton>
70+
</template>
4771
</li>
4872
</template>
4973

5074
<script>
75+
import { DialogBuilder } from '@nextcloud/dialogs'
5176
import { ShareType } from '@nextcloud/sharing'
5277
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
5378
import NcButton from '@nextcloud/vue/components/NcButton'
5479
import NcSelect from '@nextcloud/vue/components/NcSelect'
80+
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
5581
import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue'
82+
import PencilIcon from 'vue-material-design-icons/Pencil.vue'
5683
import ShareExpiryTime from './ShareExpiryTime.vue'
5784
import SharingEntryQuickShareSelect from './SharingEntryQuickShareSelect.vue'
5885
import ShareDetails from '../mixins/ShareDetails.js'
5986
import SharesMixin from '../mixins/SharesMixin.js'
87+
import logger from '../services/logger.ts'
88+
import { openShareEditDialog } from '../services/SharingDialog.ts'
6089
6190
export default {
6291
name: 'SharingEntry',
6392
6493
components: {
6594
NcButton,
6695
NcAvatar,
96+
DeleteIcon,
6797
DotsHorizontalIcon,
98+
PencilIcon,
6899
NcSelect,
69100
ShareExpiryTime,
70101
SharingEntryQuickShareSelect,
@@ -128,6 +159,54 @@ export default {
128159
return (typeof this.share.status === 'object' && !Array.isArray(this.share.status))
129160
},
130161
},
162+
163+
methods: {
164+
/**
165+
* Open the unified sharing dialog to edit this share.
166+
*/
167+
async openEditDialog() {
168+
try {
169+
await openShareEditDialog(this.share.id, this.fileInfo.node)
170+
} catch (error) {
171+
logger.error('Failed to open the sharing dialog', { error })
172+
}
173+
},
174+
175+
/**
176+
* Ask for confirmation before deleting the share.
177+
*/
178+
async confirmDelete() {
179+
let confirmed = false
180+
const dialog = (new DialogBuilder())
181+
.setName(t('files_sharing', 'Delete share'))
182+
.setText(t('files_sharing', 'Are you sure you want to delete this share? This operation cannot be undone.'))
183+
.setButtons([
184+
{
185+
label: t('files_sharing', 'Cancel'),
186+
variant: 'secondary',
187+
callback: () => {},
188+
},
189+
{
190+
label: t('files_sharing', 'Delete'),
191+
variant: 'error',
192+
callback: () => {
193+
confirmed = true
194+
},
195+
},
196+
])
197+
.build()
198+
199+
try {
200+
await dialog.show()
201+
} catch (error) {
202+
logger.debug('Delete confirmation dialog closed', { error })
203+
}
204+
205+
if (confirmed) {
206+
this.onDelete()
207+
}
208+
},
209+
},
131210
}
132211
</script>
133212

apps/files_sharing/src/components/SharingEntryLink.vue

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
{{ subtitle }}
2222
</p>
2323
<SharingEntryQuickShareSelect
24-
v-if="share && share.permissions !== undefined"
24+
v-if="share && share.permissions !== undefined && !config.sharingDialogEnabled"
2525
:share="share"
2626
:file-info="fileInfo"
2727
@open-sharing-details="openShareDetailsForCustomSettings(share)" />
@@ -151,6 +151,7 @@
151151
<template v-if="share">
152152
<template v-if="share.canEdit && canReshare">
153153
<NcActionButton
154+
v-if="!config.sharingDialogEnabled"
154155
:disabled="saving"
155156
:close-after-click="true"
156157
@click.prevent="openSharingDetails">
@@ -159,6 +160,16 @@
159160
</template>
160161
{{ t('files_sharing', 'Customize link') }}
161162
</NcActionButton>
163+
<NcActionButton
164+
v-else
165+
:disabled="saving"
166+
:close-after-click="true"
167+
@click.prevent="openEditDialog">
168+
<template #icon>
169+
<Tune :size="20" />
170+
</template>
171+
{{ t('files_sharing', 'Customize link') }}
172+
</NcActionButton>
162173
</template>
163174

164175
<NcActionButton
@@ -279,6 +290,7 @@ import ShareDetails from '../mixins/ShareDetails.js'
279290
import SharesMixin from '../mixins/SharesMixin.js'
280291
import Share from '../models/Share.ts'
281292
import logger from '../services/logger.ts'
293+
import { openShareEditDialog } from '../services/SharingDialog.ts'
282294
import GeneratePassword from '../utils/GeneratePassword.ts'
283295
284296
export default {
@@ -599,6 +611,17 @@ export default {
599611
},
600612
601613
methods: {
614+
/**
615+
* Open the unified sharing dialog to edit this link share.
616+
*/
617+
async openEditDialog() {
618+
try {
619+
await openShareEditDialog(this.share.id, this.fileInfo.node)
620+
} catch (error) {
621+
logger.error('Failed to open the sharing dialog', { error })
622+
}
623+
},
624+
602625
/**
603626
* Check if the share requires review
604627
*
@@ -866,6 +889,7 @@ export default {
866889
justify-content: space-between;
867890
flex: 1 0;
868891
min-width: 0;
892+
align-items: center;
869893
}
870894
871895
&__desc {

apps/files_sharing/src/services/ConfigService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
import { getCapabilities } from '@nextcloud/capabilities'
66
import { loadState } from '@nextcloud/initial-state'
7+
import { isSharingDialogAvailable } from './SharingDialog.ts'
78

89
type PasswordPolicySettings = {
910
enforceNonCommonPassword: boolean
@@ -354,4 +355,13 @@ export default class Config {
354355
get showExternalSharing(): boolean {
355356
return loadState('files_sharing', 'showExternalSharing', true)
356357
}
358+
359+
/**
360+
* Whether the new unified sharing dialog replaces the legacy inline sharing UI.
361+
* Derived from the server capabilities: when the unified sharing API is not
362+
* advertised (capability empty), the legacy inputs and menus are used instead.
363+
*/
364+
get sharingDialogEnabled(): boolean {
365+
return isSharingDialogAvailable()
366+
}
357367
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { Node } from '@nextcloud/files'
7+
8+
import { getCapabilities } from '@nextcloud/capabilities'
9+
10+
/**
11+
* The unified sharing dialog is a Vue 3 component. It is registered on the
12+
* global `OCA.Sharing` namespace by the Vue 3 bridge entry point
13+
* (`sharing-dialog-bridge.ts`), so this Vue 2 frontend can trigger it without
14+
* pulling Vue 3 into its bundle.
15+
*/
16+
type SharingDialogApi = {
17+
openSharingDialog(node: Node): Promise<unknown>
18+
openShareEditDialog(shareId: string | number, node?: Node): Promise<unknown>
19+
}
20+
21+
/**
22+
* Get the sharing dialog API registered by the Vue 3 bridge, if loaded.
23+
*/
24+
function sharingDialogApi(): SharingDialogApi | undefined {
25+
return (window.OCA?.Sharing as Partial<SharingDialogApi> | undefined) as SharingDialogApi | undefined
26+
}
27+
28+
/**
29+
* Whether the server provides the unified sharing API this dialog talks to.
30+
* Mirrors the library check so the sidebar can gate on it without importing
31+
* the Vue 3 code.
32+
*/
33+
export function isSharingDialogAvailable(): boolean {
34+
const capabilities = getCapabilities() as { sharing?: { api_versions?: unknown[] } }
35+
return (capabilities.sharing?.api_versions?.length ?? 0) > 0
36+
}
37+
38+
/**
39+
* Open the unified sharing dialog to create a new share for a node.
40+
*
41+
* @param node The file or folder to share
42+
*/
43+
export function openShareCreateDialog(node: Node): Promise<unknown> {
44+
const api = sharingDialogApi()
45+
if (!api?.openSharingDialog) {
46+
return Promise.reject(new Error('The unified sharing dialog is not available'))
47+
}
48+
return api.openSharingDialog(node)
49+
}
50+
51+
/**
52+
* Open the unified sharing dialog to edit an existing share.
53+
*
54+
* @param shareId The share id (mapped to the unified API by the legacy bridge)
55+
* @param node The backing node, used for the dialog title
56+
*/
57+
export function openShareEditDialog(shareId: string | number, node?: Node): Promise<unknown> {
58+
const api = sharingDialogApi()
59+
if (!api?.openShareEditDialog) {
60+
return Promise.reject(new Error('The unified sharing dialog is not available'))
61+
}
62+
return api.openShareEditDialog(shareId, node)
63+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { Node } from '@nextcloud/files'
7+
8+
import { getShare, isSharingDialogAvailable, openSharingDialog } from '@nextcloud/sharing/dialog'
9+
10+
/**
11+
* Bridge the Vue 3 unified sharing dialog into the global `OCA.Sharing`
12+
* namespace so the (still Vue 2) files_sharing sidebar can trigger it without
13+
* importing Vue 3 code into its bundle. The dialog spawns its own Vue 3 app,
14+
* so it must be registered from this Vue 3 entry point where `vue` and
15+
* `@nextcloud/vue` resolve to their Vue 3 versions.
16+
*
17+
* Once files_sharing is migrated to Vue 3 this bridge can be dropped and the
18+
* library imported directly.
19+
*/
20+
21+
window.OCA ??= {}
22+
window.OCA.Sharing ??= {}
23+
24+
Object.assign(window.OCA.Sharing, {
25+
openSharingDialog,
26+
27+
/**
28+
* Open the unified sharing dialog to edit an existing share.
29+
*
30+
* @param shareId The share id (mapped to the unified API by the legacy bridge)
31+
* @param node The backing node, used for the dialog title
32+
*/
33+
async openShareEditDialog(shareId: string | number, node?: Node): Promise<unknown> {
34+
const share = await getShare(String(shareId))
35+
return share.showDialog(node)
36+
},
37+
38+
isSharingDialogAvailable,
39+
})

0 commit comments

Comments
 (0)