Skip to content

Commit 45c3545

Browse files
committed
fix(dialog): match the merged per-recipient permission API
nextcloud/server#63854 landed per-recipient permissions with a different shape than assumed, so align with it: - SharingRecipient exposes `permissions` only; there is no per-recipient preset field, so the selected preset is derived from the enabled permissions. - updateShareRecipientPermission sends recipientClass / recipientValue / recipientInstance / permissionClass / enabled. - There is no per-recipient preset endpoint: drop it, and apply a preset by toggling each permission to match it (skipping any beyond the share maximum). Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
1 parent f8f3eec commit 45c3545

10 files changed

Lines changed: 60 additions & 69 deletions

File tree

lib/dialog/api/share.spec.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ vi.mock('./sharing.ts', () => ({
2323
updateSharePermission: vi.fn(),
2424
selectSharePermissionPreset: vi.fn(),
2525
updateShareRecipientPermission: vi.fn(),
26-
selectShareRecipientPermissionPreset: vi.fn(),
2726
updateShareState: vi.fn(),
2827
searchRecipients: vi.fn(),
2928
deleteShare: vi.fn(),
@@ -128,13 +127,6 @@ describe('Share', () => {
128127
expect(mocked.updateShareRecipientPermission).toHaveBeenCalledWith('abc', 'R', 'bob', 'C', true, 'inst')
129128
})
130129

131-
it('selectRecipientPreset forwards recipient identity and preset', async () => {
132-
const instance = await makeShare()
133-
mocked.selectShareRecipientPermissionPreset.mockResolvedValue(share())
134-
await instance.selectRecipientPreset('R', 'bob', 'PresetX')
135-
expect(mocked.selectShareRecipientPermissionPreset).toHaveBeenCalledWith('abc', 'R', 'bob', 'PresetX', undefined)
136-
})
137-
138130
it('addSource forwards the class and value', async () => {
139131
const instance = await makeShare()
140132
mocked.addShareSource.mockResolvedValue(share())

lib/dialog/api/share.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -197,18 +197,6 @@ class Share {
197197
return this.#sync(await client.updateShareRecipientPermission(this.id, recipientClass, recipientValue, permissionClass, enabled, instance))
198198
}
199199

200-
/**
201-
* Apply a permission preset to one recipient.
202-
*
203-
* @param recipientClass The recipient type class
204-
* @param recipientValue The recipient value
205-
* @param presetClass The preset class to apply
206-
* @param instance The recipient's instance (federated shares)
207-
*/
208-
async selectRecipientPreset(recipientClass: string, recipientValue: string, presetClass: string, instance?: string): Promise<this> {
209-
return this.#sync(await client.selectShareRecipientPermissionPreset(this.id, recipientClass, recipientValue, presetClass, instance))
210-
}
211-
212200
/**
213201
* Set the share state (draft → active → deleted).
214202
*

lib/dialog/api/sharing.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -188,34 +188,15 @@ export async function selectSharePermissionPreset(shareId: string, presetClass:
188188
*/
189189
export async function updateShareRecipientPermission(shareId: string, recipientClass: string, recipientValue: string, permissionClass: string, enabled: boolean, instance?: string): Promise<SharingShare> {
190190
const response = await axios.put(sharingUrl(`/share/${shareId}/recipient/permission`), {
191-
class: recipientClass,
192-
value: recipientValue,
193-
instance: instance ?? null,
191+
recipientClass,
192+
recipientValue,
193+
recipientInstance: instance ?? null,
194194
permissionClass,
195195
enabled,
196196
})
197197
return unwrapOcs<SharingShare>(response)
198198
}
199199

200-
/**
201-
* Apply a permission preset to one recipient of a share.
202-
*
203-
* @param shareId
204-
* @param recipientClass
205-
* @param recipientValue
206-
* @param presetClass
207-
* @param instance
208-
*/
209-
export async function selectShareRecipientPermissionPreset(shareId: string, recipientClass: string, recipientValue: string, presetClass: string, instance?: string): Promise<SharingShare> {
210-
const response = await axios.put(sharingUrl(`/share/${shareId}/recipient/permission/preset`), {
211-
class: recipientClass,
212-
value: recipientValue,
213-
instance: instance ?? null,
214-
permissionPresetClass: presetClass,
215-
})
216-
return unwrapOcs<SharingShare>(response)
217-
}
218-
219200
/**
220201
* Update the state of a share (draft → active → deleted).
221202
*

lib/dialog/components/SharePanel.spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ function schema(overrides: Partial<SharingShare> = {}): SharingShare {
7979
{ class: PERM_READ, source_class: null, display_name: 'Read', hint: null, priority: 10, presets: [PRESET_VIEW, PRESET_EDIT], enabled: true },
8080
{ class: PERM_WRITE, source_class: null, display_name: 'Write', hint: null, priority: 20, presets: [PRESET_EDIT], enabled: false },
8181
],
82-
permission_preset: null,
8382
...overrides,
8483
}
8584
}
@@ -178,7 +177,6 @@ describe('SharePanel tab bar', () => {
178177
icon: null,
179178
secret: { updatable: false },
180179
initiator: null,
181-
permission_preset: null,
182180
permissions: [],
183181
}
184182

lib/dialog/composables/useRecipientPermissions.spec.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ function recipient(overrides: Partial<SharingRecipient> = {}): SharingRecipient
4040
icon: null,
4141
secret: { updatable: false },
4242
initiator: null,
43-
permission_preset: null,
4443
permissions: [],
4544
...overrides,
4645
}
@@ -51,7 +50,6 @@ function fakeShare(permissions: SharingPermission[], preset: string | null = nul
5150
permissions,
5251
permissionPreset: preset,
5352
data: { owner },
54-
selectRecipientPreset: vi.fn().mockResolvedValue(undefined),
5553
setRecipientPermission: vi.fn().mockResolvedValue(undefined),
5654
} as unknown as Share
5755
}
@@ -78,9 +76,14 @@ describe('permissions', () => {
7876
})
7977

8078
describe('selectedPreset', () => {
81-
test('falls back to custom when the recipient preset is not offered', () => {
82-
const { selectedPreset } = useRecipientPermissions(cappedShare(), () => recipient({ permission_preset: 'Edit' }))
83-
expect(selectedPreset.value.value).toBe(CUSTOM_VALUE)
79+
test('derives the preset from the enabled permissions', () => {
80+
const r = recipient({ permissions: [permission('read', true, ['View', 'Edit'])] })
81+
expect(useRecipientPermissions(cappedShare(), () => r).selectedPreset.value.value).toBe('View')
82+
})
83+
84+
test('falls back to custom when no preset matches the enabled permissions', () => {
85+
const r = recipient({ permissions: [permission('read', false, ['View', 'Edit'])] })
86+
expect(useRecipientPermissions(cappedShare(), () => r).selectedPreset.value.value).toBe(CUSTOM_VALUE)
8487
})
8588
})
8689

@@ -111,12 +114,17 @@ describe('mutations', () => {
111114
share = cappedShare()
112115
})
113116

114-
test('onPresetChange forwards to the share, skipping custom', async () => {
115-
const { onPresetChange } = useRecipientPermissions(share, () => recipient())
117+
test('onPresetChange applies the preset by toggling permissions', async () => {
118+
const r = recipient({ permissions: [permission('read', false, ['View', 'Edit']), permission('write', false, ['Edit'])] })
119+
const { onPresetChange } = useRecipientPermissions(share, () => r)
120+
116121
await onPresetChange({ value: CUSTOM_VALUE, label: 'Can…' })
117-
expect(share.selectRecipientPreset).not.toHaveBeenCalled()
122+
expect(share.setRecipientPermission).not.toHaveBeenCalled()
123+
118124
await onPresetChange({ value: 'View', label: 'Can view' })
119-
expect(share.selectRecipientPreset).toHaveBeenCalledWith('UserRecipient', 'carol', 'View', undefined)
125+
// read belongs to the preset and is within the share max; write is over it.
126+
expect(share.setRecipientPermission).toHaveBeenCalledWith('UserRecipient', 'carol', 'read', true, undefined)
127+
expect(share.setRecipientPermission).toHaveBeenCalledTimes(1)
120128
})
121129

122130
test('onPermissionToggle forwards to the share', async () => {

lib/dialog/composables/useRecipientPermissions.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,34 @@ export function useRecipientPermissions(share: Share, getRecipient: () => Sharin
6161
]
6262
})
6363

64-
const selectedPreset = computed<PresetOption>(() => presetOptions.value.find((option) => option.value === recipient.value.permission_preset) ?? customOption)
65-
66-
const showPermissions = computed(() => selectedPreset.value.value === CUSTOM_VALUE)
67-
6864
const permissions = computed<RecipientPermission[]>(() => {
6965
const max = shareMax.value
70-
// The backend may not (yet) return per-recipient permissions.
7166
return (recipient.value.permissions ?? []).map((permission) => ({
7267
...permission,
7368
available: max.has(permission.class),
7469
}))
7570
})
7671

72+
/**
73+
* Recipients carry permissions but no preset field, so the preset is the one
74+
* whose member permissions are exactly the enabled ones (custom otherwise).
75+
*/
76+
const selectedPreset = computed<PresetOption>(() => {
77+
const enabled = new Set(permissions.value.filter((permission) => permission.enabled).map((permission) => permission.class))
78+
for (const option of presetOptions.value) {
79+
if (option.value === CUSTOM_VALUE) {
80+
continue
81+
}
82+
const members = permissions.value.filter((permission) => permission.presets.includes(option.value))
83+
if (members.length > 0 && members.length === enabled.size && members.every((permission) => enabled.has(permission.class))) {
84+
return option
85+
}
86+
}
87+
return customOption
88+
})
89+
90+
const showPermissions = computed(() => selectedPreset.value.value === CUSTOM_VALUE)
91+
7792
/** Whether any permission is beyond the share max (drives the cap notice). */
7893
const hasCap = computed(() => permissions.value.some((permission) => !permission.available))
7994

@@ -113,11 +128,22 @@ export function useRecipientPermissions(share: Share, getRecipient: () => Sharin
113128
}
114129
presetError.value = null
115130
const r = recipient.value
116-
try {
117-
await share.selectRecipientPreset(r.class, r.value, option.value, r.instance ?? undefined)
118-
} catch (e) {
119-
logger.error('Failed to select recipient permission preset', { error: e, recipient: r.value, preset: option.value })
120-
presetError.value = getOcsErrorMessage(e)
131+
// There is no per-recipient preset endpoint: apply the preset by toggling
132+
// each permission to match it, skipping any beyond the share maximum.
133+
const target = new Set(permissions.value.filter((permission) => permission.presets.includes(option.value)).map((permission) => permission.class))
134+
const snapshot = permissions.value.map(({ class: permissionClass, enabled, available }) => ({ permissionClass, enabled, available }))
135+
for (const { permissionClass, enabled, available } of snapshot) {
136+
const shouldEnable = target.has(permissionClass)
137+
if (!available || enabled === shouldEnable) {
138+
continue
139+
}
140+
try {
141+
await share.setRecipientPermission(r.class, r.value, permissionClass, shouldEnable, r.instance ?? undefined)
142+
} catch (e) {
143+
logger.error('Failed to apply recipient permission preset', { error: e, recipient: r.value, permission: permissionClass })
144+
presetError.value = getOcsErrorMessage(e)
145+
return
146+
}
121147
}
122148
}
123149

lib/dialog/composables/useRecipientSearch.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ const bob: SharingRecipient = {
2121
icon: null,
2222
secret: { updatable: false },
2323
initiator: null,
24-
permission_preset: null,
2524
permissions: [],
2625
}
2726

lib/dialog/types/api.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ export interface SharingRecipient {
5151
icon: SharingIcon | null
5252
secret: SharingRecipientSecret
5353
initiator: SharingOwner | null
54-
/** Class of the preset matching this recipient's enabled permissions, null when custom */
55-
permission_preset: string | null
56-
/** Per-recipient permissions, capped at the share-level (maximum) permissions */
54+
/**
55+
* Per-recipient permissions, capped at the share-level (maximum) permissions.
56+
* There is no preset field: the preset is derived from the enabled ones.
57+
*/
5758
permissions: SharingPermission[]
5859
}
5960

tests/ct/fixtures/RecipientListFixture.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ function recipient(cls: string, value: string, displayName: string): SharingReci
2323
icon: null,
2424
secret: { updatable: false },
2525
initiator: null,
26-
permission_preset: null,
2726
permissions: [],
2827
}
2928
}

tests/ct/fixtures/RecipientRowFixture.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ const recipient: SharingRecipient = {
2828
icon: null,
2929
secret: { updatable: false },
3030
initiator: null,
31-
permission_preset: null,
3231
permissions: [],
3332
}
3433

0 commit comments

Comments
 (0)