Sharing & Invites
This area covers 30 features. Watch the walkthrough, then use the reference below — each feature links to the exact moment it appears (▶).
Who this is for: Project lead / owner. Each feature below lists the role/permission it requires.
Features
Generate single-project invite link ▶ 00:00
As a project owner, I want to generate a shareable invite link so that I can onboard collaborators who may not have Aquilla accounts.
How it works. User opens Share dialog → Invite link tab → role/email/expiry picker → clicks 'Create invite link' → receives a unique URL on success. User can copy the URL to clipboard via copy button. On error (permission/network), user sees error message.
Key files
src/components/SharePanel.tsx:170-367src/lib/sync/invites.ts:79-115e2e/specs/projects/share-invite-link.smoke.spec.ts
Configure invite link role ▶ 00:06
As a project owner, I want to specify the role (viewer/commenter/reviewer/contributor) that a recipient will receive so that I can grant appropriate access levels.
How it works. User opens Invite link tab → Role dropdown (aria-label='Role') shows current selection → user clicks dropdown → selects role option → trigger updates to show new role. Default is CONTRIBUTOR (400).
Key files
src/components/SharePanel.tsx:257-279src/lib/frontier/roles.ts (LINK_ROLE_OPTIONS)
Configure invite link expiry ▶ 00:00
As a project owner, I want to set when an invite link expires so that I can control the time window for joining.
How it works. User opens Invite link tab → Expiry dropdown (aria-label='Link expires') with options: 1 day, 7 days, 30 days (default), No expiry → user selects option → trigger shows new expiry. Null value means no expiry.
Key files
src/components/SharePanel.tsx:313-339src/components/SharePanel.tsx:162-167 (EXPIRY_OPTIONS)
Target invite to specific email ▶ 00:00
As a project owner, I want to send an invite to a specific email address so that the recipient can prefill their signup form and the invite is bound to their account.
How it works. User opens Invite link tab → enters email in 'Recipient email' input (optional) → regex validation triggers on blur or create attempt. If valid email entered, the join page prefills the signup form. If invalid email provided and user tries to create, error message shows: 'Enter a valid email address, or leave blank for an open link.' Valid: empty or RFC5322-pattern email.
Key files
src/components/SharePanel.tsx:286-312src/components/SharePanel.tsx:187-190 (email regex)
Copy invite URL to clipboard ▶ 00:00
As a project owner, I want to copy the invite link to my clipboard so that I can paste it into email or messaging.
How it works. After generating invite link, user sees readonly input with URL. Copy button (title='Copy URL', Copy icon) appears. User clicks button → URL copied to clipboard → 'Copied!' confirmation text appears for ~1.5 seconds.
Key files
src/components/SharePanel.tsx:219-223src/components/SharePanel.tsx:235-252
List active (unredeemed) invite links ▶ 00:13
As a project owner, I want to see all active invite links I've created so that I can manage and revoke them if needed.
How it works. Below the create-form, 'Active links' section renders if any invites exist. Shows token suffix (...last-8-chars), role, email/open-link badge, and expiry date. Empty state: section doesn't render. Loading state renders placeholder text.
Key files
src/components/SharePanel.tsx:369-486src/lib/sync/invites.ts:195-217
Revoke active invite link ▶ 00:00
As a project owner, I want to revoke an invite link before it's redeemed so that I can prevent unauthorized access.
How it works. In Active links list, user clicks Trash icon on a link → trash button replaced with confirmation checkbox + 'Revoke' button (destructive). Checkbox label: 'Confirm'. User checks → 'Revoke' enables → clicks → link is deleted (removed from list). Cancel button closes the prompt.
Key files
src/components/SharePanel.tsx:442-480src/lib/sync/invites.ts:223-247
Preview invite before redeeming ▶ 00:00
As a joiner, I want to see details about the project and role before committing to join so that I know what access I'm accepting.
How it works. User visits /join/:token (signed-out or signed-in). Invite preview loads async. If valid: card shows project name, role (capitalized, underscores to spaces), recipient email (if bound), and expiry. If expired/invalid: shows 'This invite link is no longer valid' error and 'Back to projects' button. If network error: shows 'Couldn't load invitation' + 'Try again' button.
Key files
src/components/JoinPage.tsx:56-86src/components/JoinPage.tsx:146-202src/lib/sync/invites.ts:127-146
Preview multi-project invite before redeeming ▶ 00:00
As a joiner receiving a bulk invite to multiple projects, I want to see all projects I'm being invited to so that I can understand the scope before accepting.
How it works. User visits /join/:token with multi-project token. JoinPage loads preview via previewMultiInvite. Card shows: 'You're invited to N project(s)' + bulleted list of project names (with archived badge if applicable) + role summary. After acceptance, user sees all invited projects.
Key files
src/components/JoinPage.tsx:169-194src/lib/sync/invites.ts:276-293
Sign in from invite link (inline auth) ▶ 00:00
As an unsigned-out user, I want to sign in or create an account directly from the invite link page so that I don't have to navigate elsewhere.
How it works. User arrives at /join/:token while signed out. Card shows invite preview + inline auth form. User can: login via FrontierLoginForm (username + password) or click 'Create an account' to swap to FrontierSignupForm. After successful auth, session updates and card swaps to confirmation. Forgot password link navigates to FrontierForgotPasswordForm.
Key files
src/components/JoinPage.tsx:274-320src/components/git-import/FrontierLoginForm.tsxsrc/components/git-import/FrontierSignupForm.tsx
Accept single-project invite ▶ 00:00
As a signed-in joiner, I want to confirm and accept a project invite so that I become a member and can access the project.
How it works. Signed-in user visits /join/:token. Card shows preview + 'Accept invitation' and 'Not now' buttons. User clicks 'Accept invitation' → phase becomes 'redeeming' → spinner shows → acceptServerInvite called → on success: redirects to /project/:projectId. On failure: phase='error' with message 'This invite link is no longer valid. Ask the project owner for a fresh link.'
Key files
src/components/JoinPage.tsx:100-130src/components/JoinPage.tsx:256-273src/lib/sync/invites.ts:155-178
Accept multi-project invite ▶ 00:00
As a signed-in joiner receiving a multi-project invite, I want to accept and join all projects at once so that I get access to the entire workspace set.
How it works. Signed-in user visits /join/:token with multi-project token. Card shows preview listing all projects. User clicks 'Accept invitation' → acceptMultiInvite called → on success: user is added to all projects, redirects to /project/:projectId (first project). Analytics event INVITE_REDEEMED fired with invite_kind='multi', project_count=N.
Key files
src/components/JoinPage.tsx:100-130src/lib/sync/invites.ts:295-318
Share panel Members tab ▶ 00:00
As a project owner, I want to view and manage project members directly so that I can see who has access and modify roles.
How it works. User opens Share dialog → Members tab (default active) → sees MembersPanel with: (1) existing members list showing username, role, source badge (via org/group/creator), (2) if member is locked (org/creator source), shows lock hint tooltip instead of delete button, (3) if member is unlocked, shows role-change select (capped at caller's max role) and trash delete button, (4) add-member input at top (UsernameTypeahead or direct invite).
Key files
src/components/SharePanel.tsx:95-150src/components/MembersPanel.tsx
Add member to project by username ▶ 00:00
As a project owner, I want to add an existing Aquilla user to my project by their username so that I can grant them access.
How it works. User in Members tab types username in UsernameTypeahead → dropdown shows matching users (scoped search) → user picks suggestion (sets value.resolved) → role defaults to CONTRIBUTOR → user clicks Add button (or types username, hits Enter). Call addProjectMember. On success: member appears in list. On failure: error message shows.
Key files
src/components/MembersPanel.tsx:71-84src/components/UsernameTypeahead.tsx:59-250src/hooks/useUserSearch.ts
Change member's project role ▶ 00:00
As a project owner, I want to change a member's role so that I can adjust their permissions.
How it works. In Members tab, next to a member's name is a role dropdown (aria-label='Change role'). Dropdown shows roles the caller can grant (capped at callerMaxRole). User selects new role → onChangeRole fires → role updates. If member is locked (org/creator source), dropdown is hidden and lock hint shows instead.
Key files
src/components/MembersPanel.tsx:109-134src/hooks/useProjectMembers.ts
Remove member from project ▶ 00:00
As a project owner, I want to remove a member from my project so that I can revoke their access.
How it works. In Members tab, next to unlocked member is a Trash icon button (aria-label='Remove {username}'). User clicks → onRemove fires → member disappears from list. If member is locked (org/creator), trash button hidden and lock hint tooltip shows.
Key files
src/components/MembersPanel.tsx:135-150src/hooks/useProjectMembers.ts
Add multiple users to multiple projects via dialog ▶ 00:00
As an org admin, I want to add an existing Aquilla user to multiple projects at once so that I can onboard them efficiently.
How it works. Org admin opens Members page → clicks 'Add to projects' button (disabled if no projects) → MultiProjectInviteDialog opens. User types username in UsernameTypeahead → picks suggestion (verified badge shows). User toggles project checkboxes (selectable roles shown per-project, default CONTRIBUTOR). User clicks 'Add to projects' → parallel addProjectMember calls → on success, per-project 'added' chips appear, cancel button becomes 'Close'. Dialog stays open.
Key files
src/components/MultiProjectInviteDialog.tsx:55-350src/pages/MembersPage.tsx:168-177src/lib/frontier/members.ts
Switch invite mode between username and email ▶ 00:00
As an org admin inviting via the multi-project dialog, I want to toggle between inviting existing users (@user) and sending email invites so that I can choose the right path for each recipient.
How it works. MultiProjectInviteDialog shows mode-toggle buttons: '@user' (default, username typeahead) and 'email' (email input type). User clicks 'email' → recipient input becomes type='email', description changes to 'Email invites are per-project'. Clicking '@user' swaps back to typeahead. In email mode, submit button hidden; instead guide shows 'use each project's Share panel'.
Key files
src/components/MultiProjectInviteDialog.tsx:62-77src/components/MultiProjectInviteDialog.tsx:177-181src/components/UsernameTypeahead.tsx:165-210
Verified Aquilla user badge ▶ 00:00
As a user filling an invite form, I want to see when I've picked a verified Aquilla user so that I know the username exists and is resolved.
How it works. In MultiProjectInviteDialog or SharePanel Members tab, user types username → types match a suggestion → user clicks suggestion (handlePick sets value.resolved). Verified badge appears next to input (data-tooltip='Verified Aquilla user', green background, 'Verified' text). Badge disappears if user edits the input away from the picked username.
Key files
src/components/UsernameTypeahead.tsx:160-163src/components/UsernameTypeahead.tsx:255-270
Username typeahead search ▶ 00:00
As a user adding members, I want to search for usernames as I type so that I can find the right person.
How it works. User types in username input → after 2+ chars, useUserSearch fetches /api/v2/users/search with scoped=true (org/project-overlap). Dropdown shows matching results sorted alphabetically, excluding already-added users. Sub-2-char input shows hint. No matches shows 'No Aquilla user matching that name'. Clicking result picks it (sets resolved).
Key files
src/components/UsernameTypeahead.tsx:59-250src/hooks/useUserSearch.ts
Pending invitations inbox (received invites) ▶ 00:00
As a user with pending email-targeted invites, I want to see them on my dashboard so that I can accept them even if the email link never arrived.
How it works. User lands on dashboard (/ or /projects). If they have pending invites addressed to their account email (via listMyPendingInvites), a 'Pending invitations' card renders. Card shows: project name(s), inviter name, role, expiry. 'Review & accept' link navigates to /join/:token. If no invites, card doesn't render. On failure, returns empty list (dashboard card hides).
Key files
src/components/org/OrgHome.tsx:172-173src/components/org/OrgHome.tsx:320-345 (rendering)src/lib/sync/invites.ts:337-354
Targeted vs open invite badge ▶ 00:00
As an org admin viewing pending invites, I want to see whether each invite is targeted to a specific email or an open link so that I can understand its scope.
How it works. In Members page Pending invitations section, each pending invite row shows: project name · role · [email badge OR 'open link' badge] · expiry. Targeted invite shows email in blue chip (tooltip: 'Targeted invite...'). Open invite shows 'open link' text.
Key files
src/pages/MembersPage.tsx:290-365src/components/org/OrgHome.tsx:330-340
Revoke pending invite (org admin) ▶ 00:00
As an org owner/maintainer, I want to revoke a pending invite so that I can cancel an accidental share or revoke an employee's access before they join.
How it works. In Pending invitations list (MembersPage), org owner hovers/taps invite row → X or trash button appears. User clicks → confirmation or immediate DELETE /api/v2/projects/:id/invites/:token → row disappears from list. Error message shows if revoke fails.
Key files
src/pages/MembersPage.tsx:346-390src/hooks/useOrgInvites.ts:66-87src/lib/frontier/orgs.ts (listPendingOrgInvites, revokeProjectInvite)
External collaborators governance ▶ 00:00
As an org maintainer, I want to see everyone who accesses my projects without being org members so that I can audit and revoke their access.
How it works. Members page shows 'External collaborators' section (maintainer+ only, null hides it for non-maintainers). Lists: username + 'external' badge. For each project grant, shows: project name · role · revoke button (if direct grant) OR 'via group/creator' tooltip (if group/creator source). Clicking revoke removes that project grant only (user may stay if other projects grant them).
Key files
src/components/org/ExternalCollaboratorsSection.tsxsrc/pages/MembersPage.tsx:207-212src/lib/frontier/external-collaborators.ts
Cross-org project visibility on accept ▶ 00:00
As a joiner accepting an invite to a project in another organization, I want the project to appear on my dashboard so that I can navigate to it.
How it works. User accepts invite to a project in org A (user is in org B). Project surfaces on /projects under 'Shared with you' partition. Also visible on / (dashboard Overview). Clicking navigates to /project/:id and syncs normally.
Key files
src/components/org/OrgHome.tsx:300-345src/lib/frontier/shared-projects.ts (partitionSharedProjects)src/pages/DashboardPage.tsx (Dashboard)
Invite URL copy confirmation toast ▶ 00:00
As a user copying an invite link, I want a visual confirmation so that I know the copy succeeded.
How it works. After clicking Copy URL button, 'Copied!' text appears below the input for ~1.5 seconds, then fades away.
Key files
src/components/SharePanel.tsx:219-223src/components/SharePanel.tsx:244
Onboarding invite step (simplified flow) ▶ 00:00
As a user setting up their first project, I want to invite teammates directly from the onboarding checklist so that I can quickly build a team.
How it works. Onboarding checklist shows 'Invite step'. Two paths: (1) By username: type collaborator username → click Add → 'Added {username} as contributor' success message, (2) Share link: click 'Create link' → URL appears with copy button. Both paths show onboarding-flavored UX (smaller form, simplified copy).
Key files
src/components/onboarding/checklist/InviteStep.tsx
Navigate away from pending invite ▶ 00:00
As a joiner on the invite confirmation page, I want to decline the invite and return to the dashboard so that I can explore without committing.
How it works. On JoinPage, user clicks 'Not now' button → navigates to / (dashboard root). Project is not added to their membership.
Key files
src/components/JoinPage.tsx:270-272
Invalid/expired invite error handling ▶ 00:00
As a joiner with an invalid or expired invite link, I want clear error messaging so that I know what went wrong.
How it works. User visits /join/:token with invalid token or expired invite. Preview fetch returns reason='expired' or 'invalid'. Card shows error state with AlertCircle icon. If expired/invalid: 'This invite link is no longer valid. Ask the project owner for a new invite link.' If network error: 'Couldn't load invitation' + 'Try again' button. Both show 'Back to projects' link.
Key files
src/components/JoinPage.tsx:138-254src/lib/sync/invites.ts:32-37, 127-146
Project link visibility gating ▶ 00:00
As a non-member, I want to ensure I can't access the project's sync data even with an invite link so that membership is the only gate to content.
How it works. Invite acceptance adds user to project_members. /sync-token endpoint checks membership; if user not a member, returns 401. Local-only project view still works but sync-worker writes return 401 (blocked).
Key files
src/components/JoinPage.tsx:33-40 (comments)src/lib/sync/invites.ts (comments at top)