Skip to content

Commit f8f3eec

Browse files
committed
fix(dialog): keep the share-type tabs and confirm dropping invited people
The Invited/Anyone tabs disappeared as soon as a recipient existed, including the link's own token recipient, so they vanished right after picking "Anyone". Always show the tabs instead. Switching to "Anyone" now asks for confirmation when invited people are present, and removes them, since a public link cannot keep them. Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
1 parent 6c48977 commit f8f3eec

3 files changed

Lines changed: 106 additions & 14 deletions

File tree

lib/dialog/components/SharePanel.spec.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,32 @@ vi.mock('../api/share.ts', () => ({
3535
searchRecipients: vi.fn().mockResolvedValue([]),
3636
}))
3737

38+
// Confirm dialog: always answer with the last button ("Continue").
39+
vi.mock('@nextcloud/dialogs', () => ({
40+
DialogBuilder: class {
41+
buttons: { callback: () => void }[] = []
42+
setName() {
43+
return this
44+
}
45+
46+
setText() {
47+
return this
48+
}
49+
50+
setButtons(buttons: { callback: () => void }[]) {
51+
this.buttons = buttons
52+
return this
53+
}
54+
55+
build() {
56+
const { buttons } = this
57+
return {
58+
show: async () => buttons.at(-1)?.callback(),
59+
}
60+
}
61+
},
62+
}))
63+
3864
/**
3965
* Build a share schema.
4066
*
@@ -156,14 +182,17 @@ describe('SharePanel tab bar', () => {
156182
permissions: [],
157183
}
158184

159-
it('shows the share-type tabs when there are no recipients', () => {
160-
const { wrapper } = mountPanel()
161-
expect(wrapper.findComponent({ name: 'NcRadioGroup' }).exists()).toBe(true)
185+
it('always shows the share-type tabs', () => {
186+
expect(mountPanel().wrapper.findComponent({ name: 'NcRadioGroup' }).exists()).toBe(true)
187+
const withRecipient = mountPanel(schema({ recipients: [recipient] }))
188+
expect(withRecipient.wrapper.findComponent({ name: 'NcRadioGroup' }).exists()).toBe(true)
162189
})
163190

164-
it('hides the share-type tabs once a recipient exists', () => {
165-
const { wrapper } = mountPanel(schema({ recipients: [recipient] }))
166-
expect(wrapper.findComponent({ name: 'NcRadioGroup' }).exists()).toBe(false)
191+
it('confirms and drops invited people when switching to the link tab', async () => {
192+
const { wrapper, share } = mountPanel(schema({ recipients: [recipient] }))
193+
wrapper.findComponent({ name: 'NcRadioGroup' }).vm.$emit('update:modelValue', ShareDialogTab.Anyone)
194+
await flushPromises()
195+
expect(share.removeRecipient).toHaveBeenCalledWith(RECIPIENT_TYPE_USER, 'bob', undefined)
167196
})
168197
})
169198

lib/dialog/components/SharePanel.vue

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66
<form class="share-panel" @submit.prevent>
77
<!-- First page view -->
88
<template v-if="!inSettings">
9-
<!-- Share type is committed once a recipient (or the link) exists -->
109
<NcRadioGroup
11-
v-if="!hasRecipients"
1210
class="share-panel__tab-bar"
1311
:modelValue="shareDialogTab"
1412
:label="t('Share type')"
1513
:hideLabel="true"
16-
@update:modelValue="shareDialogTab = ($event as ShareDialogTab)">
14+
@update:modelValue="onTabChange($event as ShareDialogTab)">
1715
<NcRadioGroupButton
1816
v-for="type in shareTypes"
1917
:key="type.id"
@@ -171,6 +169,7 @@ import AccountPlusOutlineIconSvg from '@mdi/svg/svg/account-plus-outline.svg?raw
171169
import IconContentCopy from '@mdi/svg/svg/content-copy.svg?raw'
172170
import IconSend from '@mdi/svg/svg/send-outline.svg?raw'
173171
import WorldMapOutlineSvg from '@mdi/svg/svg/web.svg?raw'
172+
import { DialogBuilder } from '@nextcloud/dialogs'
174173
import { computed, ref, watch } from 'vue'
175174
import NcButton from '@nextcloud/vue/components/NcButton'
176175
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
@@ -187,10 +186,10 @@ import { useLinkShare } from '../composables/useLinkShare.ts'
187186
import { usePermissionPresets } from '../composables/usePermissionPresets.ts'
188187
import { useRecipientSearch } from '../composables/useRecipientSearch.ts'
189188
import { useShareProperties } from '../composables/useShareProperties.ts'
190-
import { PROPERTY_EXPIRATION, PROPERTY_PASSWORD } from '../constants.ts'
189+
import { PROPERTY_EXPIRATION, PROPERTY_PASSWORD, RECIPIENT_TYPE_TOKEN } from '../constants.ts'
191190
import { ShareDialogTab } from '../types/ui.ts'
192191
import { getOcsErrorMessage } from '../utils/api.ts'
193-
import { t } from '../utils/l10n.ts'
192+
import { n, t } from '../utils/l10n.ts'
194193
import { logger } from '../utils/logger.ts'
195194
import { isLongTextProperty, isOptionalProperty } from '../utils/property.ts'
196195
import { shareOutcomeSummary } from '../utils/summary.ts'
@@ -218,8 +217,70 @@ const isLinkShare = computed(() => shareDialogTab.value === ShareDialogTab.Anyon
218217
// A share cannot be submitted without at least one recipient.
219218
const canSubmit = computed(() => props.share.recipients.length > 0)
220219
221-
// Once a recipient (or the link) exists, the share type is committed.
222-
const hasRecipients = computed(() => props.share.recipients.length > 0)
220+
// Invited people: every recipient except the link (token) one.
221+
const invitedRecipients = computed(() => props.share.recipients.filter((recipient) => recipient.class !== RECIPIENT_TYPE_TOKEN))
222+
223+
/**
224+
* Ask before dropping the invited people when switching to a public link.
225+
*
226+
* @param count Number of invited people that would be removed
227+
*/
228+
async function confirmDropInvited(count: number): Promise<boolean> {
229+
let confirmed = false
230+
const dialog = (new DialogBuilder())
231+
.setName(t('Share with anyone'))
232+
.setText(n(
233+
'Switching to a public link removes %n invited person from this share.',
234+
'Switching to a public link removes %n invited people from this share.',
235+
count,
236+
))
237+
.setButtons([
238+
{
239+
label: t('Cancel'),
240+
variant: 'secondary',
241+
callback: () => {},
242+
},
243+
{
244+
label: t('Continue'),
245+
variant: 'primary',
246+
callback: () => {
247+
confirmed = true
248+
},
249+
},
250+
])
251+
.build()
252+
try {
253+
await dialog.show()
254+
} catch (e) {
255+
logger.debug('Share type confirmation dialog closed', { error: e })
256+
}
257+
return confirmed
258+
}
259+
260+
/**
261+
* Switch the share type. A public link cannot keep invited people, so confirm
262+
* and remove them first.
263+
*
264+
* @param tab The tab to switch to
265+
*/
266+
async function onTabChange(tab: ShareDialogTab) {
267+
if (tab === shareDialogTab.value) {
268+
return
269+
}
270+
if (tab === ShareDialogTab.Anyone && invitedRecipients.value.length > 0) {
271+
if (!await confirmDropInvited(invitedRecipients.value.length)) {
272+
return
273+
}
274+
for (const recipient of invitedRecipients.value) {
275+
try {
276+
await props.share.removeRecipient(recipient.class, recipient.value, recipient.instance ?? undefined)
277+
} catch (e) {
278+
logger.error('Failed to remove recipient while switching to a link share', { error: e, recipient: recipient.value })
279+
}
280+
}
281+
}
282+
shareDialogTab.value = tab
283+
}
223284
224285
// Editable properties, permissions/presets, recipient search and link handling
225286
// live in dedicated composables; this component wires them to the template.

vitest.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export default defineConfig({
1919
// @nextcloud/vue ships ESM + CSS that must be transformed by Vite in tests.
2020
server: {
2121
deps: {
22-
inline: [/@nextcloud\/vue/],
22+
// @nextcloud/dialogs pulls @nextcloud/vue components (and their CSS),
23+
// both must be transformed by Vite in tests.
24+
inline: [/@nextcloud\/vue/, /@nextcloud\/dialogs/],
2325
},
2426
},
2527
coverage: {

0 commit comments

Comments
 (0)