From 3e786daea5db17850b112c00f23209b82aa89fee Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Mon, 25 May 2026 23:17:21 +0200 Subject: [PATCH] fix(grants): correct the removal of a users from a grant --- static/js/components/shareModal.js | 31 ++++++++++++------- static/js/core/types.js | 5 +-- .../js/views/sharedWithMe/sharedWithMeView.js | 2 +- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/static/js/components/shareModal.js b/static/js/components/shareModal.js index 971ad123..344bf8bf 100644 --- a/static/js/components/shareModal.js +++ b/static/js/components/shareModal.js @@ -66,7 +66,8 @@ function _buildMembers(grantList) { const members = []; for (const subjectGrants of bySubject.values()) { members.push({ - grant: subjectGrants[0], // representative grant (used for subject info) + grant: subjectGrants[0], // representative grant (used for subject/resource info) + _grants: subjectGrants, // all grants — needed to revoke every permission on remove role: _roleFromGrants(subjectGrants), _op: 'keep' }); @@ -442,15 +443,18 @@ const shareModal = { _commitStagedUsers() { for (const contact of this._stagedUsers) { + /** @type {Grant} */ + const placeholderGrant = { + id: '', // not yet persisted + granted_at: 0, + granted_by: '', + subject: { type: 'user', id: contact.id }, + permission: /** @type {import('../core/types.js').PermissionTypeEnum} */ (ROLE_PERMISSIONS[this._stagedRole][0]), + resource: { type: this._itemType, id: this._item?.id ?? '' } + }; this._localMembers.push({ - grant: { - id: '', // not yet persisted - granted_at: 0, // placeholder — grant hasn't been persisted yet - granted_by: '', - subject: { type: 'user', id: contact.id }, - permission: /** @type {import('../core/types.js').PermissionTypeEnum} */ (ROLE_PERMISSIONS[this._stagedRole][0]), - resource: { type: this._itemType, id: this._item?.id ?? '' } - }, + grant: placeholderGrant, + _grants: [], // no server grants yet — nothing to revoke on remove role: this._stagedRole, _op: 'new' }); @@ -480,7 +484,7 @@ const shareModal = { */ _renderMemberGroupsInto(container) { container.replaceChildren(); - const groups = /** @type {ShareRoleEnum[]} */ (['admin', 'editor', 'viewer']); + const groups = /** @type {ShareRoleEnum[]} */ (['viewer', 'editor', 'admin']); let memberIndex = 0; for (const role of groups) { @@ -946,8 +950,11 @@ const shareModal = { try { // ── Grants ───────────────────────────────────────────────────────── for (const m of this._localMembers) { - if (m._op === 'remove' && m.grant.id) { - await grants.revokeGrant(m.grant.id); + if (m._op === 'remove') { + // Revoke every individual grant for this subject (one per permission). + for (const g of m._grants) { + if (g.id) await grants.revokeGrant(g.id); + } } else if (m._op === 'change' && m.grant.id) { await grants.updateRole({ subject: { type: m.grant.subject.type, id: m.grant.subject.id }, diff --git a/static/js/core/types.js b/static/js/core/types.js index 04963e6d..2aafe956 100644 --- a/static/js/core/types.js +++ b/static/js/core/types.js @@ -366,8 +366,9 @@ /** * One collaborator row in the share modal's People section. * @typedef {Object} MemberEntry - * @property {Grant} grant - The underlying grant (id, subject, resource, etc.) - * @property {ShareRoleEnum} role - Derived role label shown in the UI. + * @property {Grant} grant - Representative grant (used for subject/resource info). + * @property {Grant[]} _grants - All grants for this subject on the resource (may be > 1). + * @property {ShareRoleEnum} role - Derived role label shown in the UI. * @property {'keep'|'remove'|'change'|'new'} _op - Pending local operation. */ diff --git a/static/js/views/sharedWithMe/sharedWithMeView.js b/static/js/views/sharedWithMe/sharedWithMeView.js index 8e0bc0f5..6cc68ad6 100644 --- a/static/js/views/sharedWithMe/sharedWithMeView.js +++ b/static/js/views/sharedWithMe/sharedWithMeView.js @@ -93,7 +93,7 @@ const sharedWithMeView = { if (data.items.length === 0 && !this._nextCursor) { // First page came back empty ui.showError(` - +

${i18n.t('sharedwithme_emptyStateTitle', 'Nothing shared with you yet')}

${i18n.t('sharedwithme_emptyStateDesc', 'Items shared with you by other users will appear here')}

`);