Repository navigation
Feature: search for people to share with, select several, and keep the permission checkboxes #519
Closed
branch1337
started this conversation in
Ideas
Replies: 1 comment 1 reply
|
Thank you for the suggestion. The share window no longer loads every account. A search starts at two characters and lists up to 10 people whose username or email matches; email is searched only when usernames are shown. Several people can be selected and shared with at once, and the permission checkboxes keep their values until the window is closed. With usernames hidden, an exact username is still required. This will be included in the next release. |
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Description
With
SHOW_USERNAMES_IN_UIenabled, opening the share modal loads every other account and renders one card per account under "Share with User". A small instance shows a few cards. An instance with hundreds of accounts runs the same path: one response with every username and email, and one card each inside the scroller. The search box only filters that already-loaded list. Finding one person means scrolling or filtering a directory that was fetched up front.Sharing with the next person repeats the permission step. The grantor checks "Allow editing" or "Allow re-sharing", clicks a card, and that person appears under "Already Shared With". The click then clears both checkboxes. The next person needs the same boxes checked again. Ten recipients with the same permissions are ten searches through the directory and ten times re-checking the boxes.
The scroller itself is capped (
max-h-[50vh]below thesmbreakpoint,max-h-64fromsmup). The modal does not grow to the height of hundreds of cards. The cost is the full directory request, the hundreds of cards inside that cap, and the checkbox reset after every successful add.Affected Version
v0.10.16-alpha
Requested Feature
The "Share with User" section is a search, a short result list, and a selected set.
The modal does not fetch or render the directory when it opens. The result area stays a one-line hint until the query has at least two characters. From two characters, the server returns at most 10 matches, ordered by username, matching the username or the email. The grantor's own account is absent, as it is today. Accounts that already have this recording, including the owner row, are left out of the results. A query that matches more than 10 people shows the first 10 and the grantor narrows the text.
Clicking a result selects that person. The click does not create a share and does not change the permission checkboxes. The selected people appear as removable chips under the search box. Clicking the result again, or the chip's remove control, deselects them. At most 25 people can be selected. Add is enabled when at least one person is selected. Add grants the current checkboxes to every selected person in one request, appends the new rows under "Already Shared With", clears the chips and the query, and leaves the checkboxes as they are.
The checkboxes start unchecked each time the modal opens. They stay at whatever the grantor set while the modal remains open, including after a successful Add and after a failed one. Closing the modal discards the query, the results, and the chips.
A name that already has a share is reported and skipped. The other selected people in that request still receive the share. The existing row's flags stay as they are.
The exact-username field used when
SHOW_USERNAMES_IN_UIis false stays one username per Add. Public links, group-tag shares, and group-folder shares stay as they are. The "Already Shared With" list keeps its own capped scroller.Proposed Implementation
search_users(src/api/shares.py:396-429) is the lookup the share modal already uses. Stop the branch that returns every user. A query shorter than 2 characters returns[]in both username modes. When usernames are visible and the query has at least 2 characters, match username or email with the existing literalcontains_pattern, keeplimit(10), and order by username:contains_pattern(src/utils/search.py:22-24) is the pattern the username branch already uses, so%and_stay literal, whichtest_users_search_matches_underscore_literallycovers. The hidden-username==branch stays. The JSON shape staysid,username, andemail(email only when usernames are visible,src/api/shares.py:425-429).share_recording_internal(src/api/shares.py:432-545) is the only creator of a manual internal share. When the JSON object containsuser_ids, grant that list there and return. Theuser_idbranch stays the one-recipient path, including201,409already_shared, and the other current status codes.update_share(src/api/shares.py:368-381) stays the public-link settings update.At the start of the
tryinshare_recording_internal, whenisinstance(data, dict) and 'user_ids' in data:A body that is not a JSON object keeps the current handler. The
user_idbranch calls_grant_manual_internal_sharefor the insert and the inbox row, then returns the same status codes it returns today: missing id400user_id_required, unknown id404target_user_not_found, owner400share_with_owner, self400share_with_self, existing row409already_shared, ceiling failure403, success201withshare. On asharedbatch row, includeshare.to_dict()after the commit.already_sharedandnot_foundrows containuser_idandstatusonly.In
static/js/modules/composables/sharing.js, delete the directory load.openUnifiedShareModalcurrently callsloadAllUsersand copiesallUsersinto the results (static/js/modules/composables/sharing.js:535-538). Remove that call,loadAllUsers(static/js/modules/composables/sharing.js:397-417), and theallUsersstate.closeUnifiedShareModal(static/js/modules/composables/sharing.js:542-550) also clears the new selected list.searchInternalShareUsers(static/js/modules/composables/sharing.js:419-438) is the handler on the visible-username input (templates/modals/unified-share-modal.html:111-112). Replace the local filter ofallUserswith the debouncedGET /api/users/search?q=that the hidden-username branch already uses (static/js/modules/composables/sharing.js:441-466). A query shorter than 2 characters clears the results and does not request. After the response, drop ids that are already inrecordingInternalSharesor in the selected chips. The local email match atstatic/js/modules/composables/sharing.js:433-436goes away because the server query now matches email.The card click at
templates/modals/unified-share-modal.html:129-130currently callscreateInternalShare. Change it to toggle the user in a selected array. Render those users as chips between the result list and the permission block (templates/modals/unified-share-modal.html:168). Add a button, disabled while the selection is empty or a share is in flight, that posts:On
200, reload the share list once and refresh the recording share count once, the same two callscreateInternalSharemakes atstatic/js/modules/composables/sharing.js:642-645. Toast with the count ofsharedrows. Drop chips that weresharedoralready_shared. Leavenot_found,share_with_self, andshare_with_ownerselected and report them. Do not assigninternalSharePermissions. Delete the reset atstatic/js/modules/composables/sharing.js:638-639socreateInternalShare, still used byshareWithUsername(static/js/modules/composables/sharing.js:605), also keeps the checkboxes.openUnifiedShareModalstill sets both flags to false when the modal opens (static/js/modules/composables/sharing.js:473).The result list keeps the capped grid classes at
templates/modals/unified-share-modal.html:128. It renders only the current search page, at most 10 cards. Below two characters it shows a hint, not the "no users" empty state.Add
user_id_or_user_ids,user_ids_required, anduser_ids_limitnext toalready_sharedinserver.apiError(static/locales/en.json:3129). Add the hint, the Add label, the selection cap, and the summary toast insharing(static/locales/en.json:2420) and the same keys inde.json,es.json,fr.json,pt-BR.json,ru.json, andzh.json.Update "Sharing a Recording with Another User" (
docs/user-guide/sharing.md:30anddocs/user-guide/sharing.md:40): the modal asks for at least two characters, returns a short match list, and the grantor selects one or more people and presses Add. The permission checkboxes apply to that selection and stay checked until the modal is opened again. Indocs/admin-guide/group-management.md:34anddocs/admin-guide/system-settings.md:147, say that visible usernames are searched and that the share modal does not load every account.There is no browser test harness for this modal. The server tests live in
tests/test_shares_authz.py, next totest_users_search_matches_underscore_literallyandtest_internal_share_owner_can_share_and_recipient_sees_it:test_users_search_short_query_returns_no_directory: withSHOW_USERNAMES_IN_UItrue,qempty andqof one character return[]while other users exist.test_users_search_limits_and_orders_matches: 11 usernames that contain the same token, the response has 10 rows, ordered by username, and the caller is absent.test_users_search_matches_email: a query that occurs in the email and not in the username returns that user when usernames are visible.test_users_search_hidden_still_exact: with the flag false, a prefix returns[]and the full username returns that one user, withemailnull.test_share_internal_user_ids_batch: two ids,can_edittrue,200, twosharedresults, two manual rows with those flags, both recipients listed byshares-internal.test_share_internal_user_ids_already_shared_does_not_block_others: the existing row keeps its flags, the new id isshared.test_share_internal_user_ids_unknown_is_not_found: an id with no user isnot_foundand creates no row, and a real id in the same list is shared.test_share_internal_user_ids_dedupes: the same id twice creates one row.test_share_internal_user_ids_rejects_over_limit: 26 ids returns400user_ids_limitand creates no row.test_share_internal_user_ids_and_user_id_rejected: both fields returns400and creates no row.test_share_internal_user_ids_above_ceiling_denied: a grantor who lackscan_editgets403and no rows.test_share_internal_user_ids_third_party_denied: a caller who cannot reshare gets403or404and no rows, matchingtest_internal_share_nonowner_without_reshare_denied.test_internal_share_owner_can_share_and_recipient_sees_itstill returns201for a body that has onlyuser_id.Run them with
python -m pytest tests/test_shares_authz.py -k "users_search or share_internal_user_ids or internal_share_owner_can_share"from the repository root.Expected Behavior
GET /api/users/search?q=. No account cards are rendered yet.id,username, andemail. A username or an email that contains the text matches. The caller is not in the list. People already on this recording are not shown. The grid scrolls inside the existingmax-h-64cap and never holds more than those 10 cards.can_edittrue. The response is200with one result per id. Those three appear under "Already Shared With". The chips and the query clear. "Allow editing" stays checked. A second selection can be added with the same flags without checking the box again.static/js/modules/composables/sharing.js:473).already_shared. That row's flags stay. The other ids in the request are still shared.unique_recording_share(src/models/sharing.py:87-91) still forbids a second row.403withreshare_edit_not_permittedorreshare_reshare_not_permitted(src/models/sharing.py:165-170) and creates no row. A caller who cannot reshare gets the refusal the single-user POST already returns (src/api/shares.py:453-454).user_idstogether withuser_idreturns400and creates nothing.POSTwith onlyuser_idstill returns201and{success, share}, as intest_internal_share_owner_can_share_and_recipient_sees_it(tests/test_shares_authz.py:547-560). New shares still start in the recipient inbox (src/api/shares.py:506-517) and still write aShareAuditLogcreate row.shareWithUsername. A short query still returns[](src/api/shares.py:408-410). The "Already Shared With" scroller stays the block attemplates/modals/unified-share-modal.html:219.Root Cause Analysis
The visible picker is filled from the whole user table.
openUnifiedShareModalcallsloadAllUserswhenevershowUsernamesInUIis true (static/js/modules/composables/sharing.js:535-538). That function requestsGET /api/users/search?q=(static/js/modules/composables/sharing.js:402).search_userstreats a query shorter than 2 characters, when usernames are visible, as "return every user except the caller" (src/api/shares.py:405-407). The template then loopsinternalShareSearchResultsinto a card grid (templates/modals/unified-share-modal.html:128-142).searchInternalShareUsersdoes not ask the server in this mode. It filtersallUsersin the browser, including by email (static/js/modules/composables/sharing.js:422-437).The same endpoint already has a bounded query, and the modal does not use it for this screen. When the query has at least 2 characters and usernames are visible,
search_usersappliesilike(contains_pattern(query))andlimit(10)(src/api/shares.py:412-417). That branch matches username only, has noorder_by, and is reached from the hidden-username half ofsearchInternalShareUsers(static/js/modules/composables/sharing.js:441-451). The privacy input does not call that function. It callsshareWithUsername(templates/modals/unified-share-modal.html:150-155).The checkbox reset is explicit. The card click calls
createInternalShare(templates/modals/unified-share-modal.html:130). After a successfulPOSTof oneuser_id(src/api/shares.py:441-446), that function setsinternalSharePermissionsback to{ can_edit: false, can_reshare: false }(static/js/modules/composables/sharing.js:638-639). The checkboxes are bound to those two fields (templates/modals/unified-share-modal.html:172-185). The only other write is the open path, which clears them once per open (static/js/modules/composables/sharing.js:473). The user guide tells the grantor to check the boxes and then click the username, and says the share takes effect on that click (docs/user-guide/sharing.md:40). The admin guide describes visible usernames as the way colleagues are found (docs/admin-guide/group-management.md:34,docs/admin-guide/system-settings.md:147).Additional Context
The permission checkboxes are the grant for the next Add. They are not an editor for people already listed under "Already Shared With". A batch result of
already_shareddoes not change that row. Revoke stays the delete onDELETE /api/internal-shares/<id>.This request does not paginate "Already Shared With". That list is a separate scroller with the same height cap (
templates/modals/unified-share-modal.html:219). It does not change public link creation, and it does not change shares created by a group tag or a group folder. There is no/api/v1operation for these session routes, sospeakr-api-openapi.jsonstays as it is.GET /api/users/searchstill requires login and still returns403when internal sharing is off (tests/test_shares_authz.py:454-466). Returning at most 10 matches replaces the unbounded directory response. Email is included only whenSHOW_USERNAMES_IN_UIis true, which is the response the full directory already builds (src/api/shares.py:428).All reactions