Skip to content

feat(dialog): multiple recipients with per-recipient permissions - #258

Merged
skjnldsv merged 20 commits into
mainfrom
feature/dialog-per-recipient-permissions
Sep 2, 2026
Merged

feat(dialog): multiple recipients with per-recipient permissions#258
skjnldsv merged 20 commits into
mainfrom
feature/dialog-per-recipient-permissions

Conversation

@skjnldsv

@skjnldsv skjnldsv commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds multiple recipients to the sharing dialog and lets each recipient carry its own permissions.

The share-level permissions stay the default and the maximum: a new recipient inherits them, and a recipient can never be elevated above what the share grants. The backend enforces the real cap (the sharer's own permissions on a reshare, or the admin default); the dialog reflects it and explains it with a notice when something is not grantable.

What changed

  • Multiple recipients: the picker is add-only and multi-select; the authoritative list renders below it, one row per recipient.
  • Per-recipient permissions: each row shows its current preset as a static label, with the presets and a custom-permissions modal in its three-dot menu. Recipient permissions are sparse overrides on the share's permissions, so a row shows the share default until it is changed by hand; the share's own preset is marked (default).
  • Share type: the invited/anyone tabs only show while the share is a draft. Switching an existing draft to a link asks for confirmation first, because it drops the invited people.
  • Editing an existing share: the dialog opens on the view matching the share. Previously it always opened on the invited-people view, which removed the token recipient of a link share — opening a link share to edit it destroyed its public link.
  • Layout: the action bars float at the bottom of the panel, the settings view gets its own delete action, and the dialog has a minimum height so it stops jumping between states.

Testing

  • npm test — Vitest, including new coverage for the destructive paths: declining a confirmation must not delete the share or drop recipients, a failed deletion must not report success, and a recipient row must remove the recipient it renders.
  • npm run test:ct — Playwright component tests for the recipient rows and the permission editor.
  • Manually, against a server with sharing.unified_api_enable set: add several recipients, change one recipient's permissions, confirm the share max caps them, open an existing link share from the sidebar and confirm its link survives.

AI disclosure

This pull request was written with AI assistance (Claude Code); every commit carries an Assisted-by trailer. The code was reviewed by me before submission.

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.95876% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.06%. Comparing base (2c97b0a) to head (bf24a43).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
lib/dialog/components/RecipientRow.vue 67.74% 7 Missing and 3 partials ⚠️
lib/dialog/components/PermissionEditor.vue 71.42% 5 Missing and 1 partial ⚠️
lib/dialog/components/RecipientList.vue 0.00% 5 Missing and 1 partial ⚠️
lib/dialog/composables/useRecipientPermissions.ts 91.04% 6 Missing ⚠️
lib/dialog/components/SharePanel.vue 90.00% 4 Missing and 1 partial ⚠️
lib/dialog/composables/useLinkShare.ts 80.00% 1 Missing ⚠️
lib/dialog/composables/useRecipientSearch.ts 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #258      +/-   ##
==========================================
+ Coverage   73.93%   79.06%   +5.12%     
==========================================
  Files          23       27       +4     
  Lines         564      726     +162     
  Branches      168      202      +34     
==========================================
+ Hits          417      574     +157     
- Misses        126      133       +7     
+ Partials       21       19       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The dialog now accepts multiple recipients: the search field is an add-only
picker and selected recipients render as a list, each with its own permission
preset selector and fine-grained toggles.

The share-level permission acts as the maximum and the default for newly added
recipients. A recipient's available presets and toggles are capped at the
permissions the share grants; toggles beyond the maximum are disabled. The
backend enforces the real cap (the sharer's own permissions on a reshare, or
the admin default).

Adds per-recipient permission methods to the Share class and API client, and
extends SharingRecipient with permission_preset + permissions.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Rework the per-recipient row to match the sidebar: avatar + name + a compact
preset dropdown, and a three-dot menu that opens a small modal for full
permission editing (preset + custom toggles) plus removal.

Extract the preset selector and fine-grained toggles into a reusable
PermissionEditor component, used both for the share-level default/maximum and
inside the per-recipient modal. The editor shows an info notice when some
permissions are capped: a reshare names the owner and the permissions granted,
otherwise it states the share's maximum. Toggles above the maximum are disabled.

A reshare is detected per recipient (its initiator differs from the share
owner).

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
…ient row

- Guard recipient.permissions (the backend does not yet return per-recipient
  permissions), which previously threw on an undefined .map().
- Recipient row: drop the inline preset select; show a static preset label
  ("Can view" / "Custom permissions"), and move preset shortcuts + custom
  permissions (opens the modal) + remove into the three-dot menu.
- Hide the Invited/Anyone tab bar once a recipient (or the link) exists.
- Constrain the "Add people" field width.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Set up Playwright component testing (@playwright/experimental-ct-vue) and cover
the recipient/permission UI in a real browser:

- PermissionEditor: notice visibility, per-permission toggles, over-cap toggles
  disabled, toggles hidden outside custom mode.
- RecipientRow (via an in-browser fixture, since a Share with methods cannot be
  passed as a serialized prop): preset subtitle, remove from the menu, and the
  custom-permissions modal opening.
- RecipientList: one row per recipient, excluding the link (token).

Specs are named *.ct.ts so Vitest (which globs *.spec/*.test) ignores them.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
- Move the Playwright component tests to tests/ct/ so they are outside the
  library's rootDir (they import fixtures outside lib, which broke the build
  and ts:check).
- Add Share.setRecipientPermission / selectRecipientPreset unit tests.
- Add SharePanel tab-bar tests (hidden once a recipient exists).
- Rename the recipient remove action to "Remove participant" and add left
  padding to the recipient list.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
…m Vitest

Follow the repo convention (*.spec.ts) for the Playwright component tests and
exclude tests/ct/ from Vitest so the two runners do not collide.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
NcSelectUsers defaults to min-width: 260px and its focus box-shadow can spill
past the dialog padding, making the "Add people" control wider than the dialog.
Pin it to the container width (min-width: 0, max-width: 100%, border-box) like
the preset select.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
@nextcloud/vue's multiple select (.select.vs--multiple) renders ~2px too wide
and overflows its container. Inset it by 1px on each side as a workaround until
it is fixed upstream in @nextcloud/vue.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
The multiple-select width is now fixed at the source in @nextcloud/vue
(NcSelect), so the local margin-inline workaround is no longer needed.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
9.11.0 includes the NcSelect multiple-variant width fix, so the recipient
picker no longer overflows the dialog.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
…ople

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>
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>
@nextcloud/vue 9.11.0 insets the multiple select with margin-inline, which
combined with an explicit width: 100% made the control 2px wider than the
dialog and introduced a horizontal scrollbar. Letting the width stay auto lets
the margins shrink it correctly.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Once a share has been sent its type is committed, so hide the Invited/Anyone
tabs for an active share and keep them (with the confirmation when dropping
invited people) while it is still a draft.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
A recipient's permissions are a sparse override map: the share's permissions
are the base (and the maximum), and a recipient entry overrides one of them. A
freshly added recipient has no overrides at all.

The dialog listed only the recipient's own entries, so a new participant showed
no permissions and picking a preset had nothing to toggle (it did nothing).
Overlay the overrides on the share's permissions instead, so recipients start
from the share's permissions and presets apply correctly.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
…modal

The modal passed through the panel's "custom preset selected" flag, so the
permission toggles were hidden whenever the recipient's permissions matched a
preset — and disappeared mid-edit as soon as the toggles happened to match one.
The modal is the custom-permission editor, so its toggles are always shown.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
@skjnldsv
skjnldsv force-pushed the feature/dialog-per-recipient-permissions branch from 8c445df to e3e8a40 Compare September 2, 2026 12:32
…ttings

- Keep the copy/send actions pinned to the bottom of the scrolling form.
- Add a (confirmed) delete action at the bottom of the settings view, pinned
  the same way; deleting closes the dialog.
- Give the form a min-height so the dialog stops jumping around as its content
  changes.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
- Give the sticky action bars an opaque background plus a top border so the
  scrolling content no longer bleeds into them.
- Settings: the delete action is compact and aligned to the end instead of
  spanning the dialog.
- Confirmation: use the success text colour so the checkmark stays readable in
  dark mode, and align "Done" to the end.
- Rename the custom permission entry from "Can…" to "Custom permissions", as
  it is called elsewhere.
- Mark the share's own preset as "(default)" in the per-recipient options, so
  hand-changed recipients are easy to spot.
- Scroll the fine-grained toggles into view when the custom entry is selected.
- Drop the tune icon from the recipient menu's custom permissions entry.

Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
The dialog always started on the invited-people view. Since that view syncs
the token recipient, opening an existing link share removed its token and so
destroyed the public link. Derive the initial view from the share instead, and
restrict the mount-time sync to adding a token so a mismatch can never delete
one.

Signed-off-by: Barthelemy Briand <barthelemy.briand@nextcloud.com>
Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
…pient rows

The confirmation dialog was mocked to always confirm, so nothing asserted that
declining keeps the share and its recipients. Make the mocked answer settable
and cover both directions, plus the removal a recipient row performs, which was
only asserted through its event and not by the recipient it acts on.

Signed-off-by: Barthelemy Briand <barthelemy.briand@nextcloud.com>
Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
@skjnldsv skjnldsv added enhancement New feature or request 3. to review Waiting for reviews labels Sep 2, 2026
@skjnldsv
skjnldsv marked this pull request as ready for review September 2, 2026 14:37
@skjnldsv
skjnldsv merged commit d1059cd into main Sep 2, 2026
17 checks passed
@skjnldsv
skjnldsv deleted the feature/dialog-per-recipient-permissions branch September 2, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant