@tuturuuu/ui 0.26.1 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/biome.json +1 -1
  3. package/package.json +48 -48
  4. package/src/components/ui/custom/combobox.tsx +4 -0
  5. package/src/components/ui/custom/workspace-access/adapters.test.ts +89 -1
  6. package/src/components/ui/custom/workspace-access/adapters.ts +67 -2
  7. package/src/components/ui/custom/workspace-access/types.ts +13 -0
  8. package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.test.tsx +79 -0
  9. package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.tsx +125 -0
  10. package/src/components/ui/custom/workspace-access/workspace-access-invite-access-picker.tsx +108 -0
  11. package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.test.tsx +134 -0
  12. package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.tsx +94 -113
  13. package/src/components/ui/custom/workspace-access/workspace-access-invite-pos-panel.tsx +75 -0
  14. package/src/components/ui/custom/workspace-access/workspace-access-invite-role-picker.tsx +151 -0
  15. package/src/components/ui/custom/workspace-access/workspace-access-labels.ts +1 -1
  16. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +27 -2
  17. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +10 -0
  18. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +102 -3
  19. package/src/components/ui/custom/workspace-access/workspace-access-role-options.test.ts +35 -0
  20. package/src/components/ui/custom/workspace-access/workspace-access-role-options.ts +21 -0
  21. package/src/components/ui/custom/workspace-select-invitations.tsx +2 -1
  22. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -1
  23. package/src/hooks/use-board-actions.test.ts +23 -0
  24. package/src/hooks/use-board-actions.ts +48 -35
  25. package/src/hooks/use-calendar-sync.tsx +12 -12
  26. package/src/hooks/use-notifications.ts +4 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.27.0](https://github.com/tutur3u/platform/compare/ui-v0.26.1...ui-v0.27.0) (2026-08-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * **tasks:** add board lifecycle actions ([3295657](https://github.com/tutur3u/platform/commit/329565797b8bb3cefdf8f3812e6f5391576bf394))
9
+ * **workspaces:** assign roles to pending invites ([#5125](https://github.com/tutur3u/platform/issues/5125)) ([d4052fd](https://github.com/tutur3u/platform/commit/d4052fd40de66bda4e9535740bbcfd5a8da19123))
10
+ * **workspaces:** revamp invitation access flow ([7ea94af](https://github.com/tutur3u/platform/commit/7ea94afc2c5e14af1c83d1478ad9c006268b13c0))
11
+ * **workspaces:** support multi-role invitations ([f6f78ba](https://github.com/tutur3u/platform/commit/f6f78bac6c2120fd70c09e85c075c4206be4f897))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **contacts:** preserve server referral search results ([8d0941d](https://github.com/tutur3u/platform/commit/8d0941de693fb784fd2487582d905f2cbd0fb16e))
17
+ * **tasks:** allow board lifecycle actions ([9a6bc0c](https://github.com/tutur3u/platform/commit/9a6bc0ce31c4191bed81607fdb5c9ee2af8e1d04))
18
+ * **workspaces:** manage roles for pending invites ([2466f6c](https://github.com/tutur3u/platform/commit/2466f6cbbd447207d87eb0e0d78b3c713b02b739))
19
+
20
+
21
+ ### Performance Improvements
22
+
23
+ * **vercel:** reduce realtime fallback polling ([e9e44a4](https://github.com/tutur3u/platform/commit/e9e44a4eb17c971beb9e1b737c4580466e427be5))
24
+ * **vercel:** reduce realtime fallback polling ([#5126](https://github.com/tutur3u/platform/issues/5126)) ([fde2692](https://github.com/tutur3u/platform/commit/fde2692ff73a45616a40b93de9fff30ffc941fd2))
25
+
3
26
  ## [0.26.1](https://github.com/tutur3u/platform/compare/ui-v0.26.0...ui-v0.26.1) (2026-08-11)
4
27
 
5
28
 
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "root": false,
3
- "$schema": "https://biomejs.dev/schemas/2.5.7/schema.json",
3
+ "$schema": "https://biomejs.dev/schemas/2.5.8/schema.json",
4
4
  "extends": "//"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuturuuu/ui",
3
- "version": "0.26.1",
3
+ "version": "0.27.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,43 +56,43 @@
56
56
  "@streamdown/math": "^1.0.2",
57
57
  "@streamdown/mermaid": "^1.0.2",
58
58
  "@tanstack/react-hotkeys": "^0.10.0",
59
- "@tanstack/react-pacer": "^0.22.1",
59
+ "@tanstack/react-pacer": "^0.23.0",
60
60
  "@tanstack/react-query": "^5.101.2",
61
61
  "@tanstack/react-table": "^9.0.0",
62
62
  "@tanstack/react-virtual": "^3.14.9",
63
- "@tiptap/core": "3.29.2",
64
- "@tiptap/extension-collaboration": "3.29.2",
65
- "@tiptap/extension-collaboration-caret": "3.29.2",
66
- "@tiptap/extension-color": "3.29.2",
67
- "@tiptap/extension-drag-handle": "3.29.2",
68
- "@tiptap/extension-drag-handle-react": "3.29.2",
69
- "@tiptap/extension-highlight": "3.29.2",
70
- "@tiptap/extension-horizontal-rule": "3.29.2",
71
- "@tiptap/extension-image": "3.29.2",
72
- "@tiptap/extension-link": "3.29.2",
73
- "@tiptap/extension-list": "3.29.2",
74
- "@tiptap/extension-node-range": "3.29.2",
75
- "@tiptap/extension-placeholder": "3.29.2",
76
- "@tiptap/extension-strike": "3.29.2",
77
- "@tiptap/extension-subscript": "3.29.2",
78
- "@tiptap/extension-superscript": "3.29.2",
79
- "@tiptap/extension-table": "3.29.2",
80
- "@tiptap/extension-table-cell": "3.29.2",
81
- "@tiptap/extension-table-header": "3.29.2",
82
- "@tiptap/extension-table-row": "3.29.2",
83
- "@tiptap/extension-text-align": "3.29.2",
84
- "@tiptap/extension-text-style": "3.29.2",
85
- "@tiptap/extension-youtube": "3.29.2",
86
- "@tiptap/pm": "3.29.2",
87
- "@tiptap/react": "3.29.2",
88
- "@tiptap/starter-kit": "3.29.2",
89
- "@tuturuuu/ai": "0.8.1",
90
- "@tuturuuu/apis": "0.11.0",
91
- "@tuturuuu/hooks": "0.1.0",
63
+ "@tiptap/core": "3.30.0",
64
+ "@tiptap/extension-collaboration": "3.30.0",
65
+ "@tiptap/extension-collaboration-caret": "3.30.0",
66
+ "@tiptap/extension-color": "3.30.0",
67
+ "@tiptap/extension-drag-handle": "3.30.0",
68
+ "@tiptap/extension-drag-handle-react": "3.30.0",
69
+ "@tiptap/extension-highlight": "3.30.0",
70
+ "@tiptap/extension-horizontal-rule": "3.30.0",
71
+ "@tiptap/extension-image": "3.30.0",
72
+ "@tiptap/extension-link": "3.30.0",
73
+ "@tiptap/extension-list": "3.30.0",
74
+ "@tiptap/extension-node-range": "3.30.0",
75
+ "@tiptap/extension-placeholder": "3.30.0",
76
+ "@tiptap/extension-strike": "3.30.0",
77
+ "@tiptap/extension-subscript": "3.30.0",
78
+ "@tiptap/extension-superscript": "3.30.0",
79
+ "@tiptap/extension-table": "3.30.0",
80
+ "@tiptap/extension-table-cell": "3.30.0",
81
+ "@tiptap/extension-table-header": "3.30.0",
82
+ "@tiptap/extension-table-row": "3.30.0",
83
+ "@tiptap/extension-text-align": "3.30.0",
84
+ "@tiptap/extension-text-style": "3.30.0",
85
+ "@tiptap/extension-youtube": "3.30.0",
86
+ "@tiptap/pm": "3.30.0",
87
+ "@tiptap/react": "3.30.0",
88
+ "@tiptap/starter-kit": "3.30.0",
89
+ "@tuturuuu/ai": "0.8.2",
90
+ "@tuturuuu/apis": "0.11.2",
91
+ "@tuturuuu/hooks": "0.1.1",
92
92
  "@tuturuuu/icons": "0.1.0",
93
- "@tuturuuu/internal-api": "0.29.0",
93
+ "@tuturuuu/internal-api": "0.30.0",
94
94
  "@tuturuuu/supabase": "0.5.0",
95
- "@tuturuuu/utils": "0.24.1",
95
+ "@tuturuuu/utils": "0.25.0",
96
96
  "@types/debug": "^4.1.13",
97
97
  "browser-image-compression": "^2.0.2",
98
98
  "class-variance-authority": "^0.7.1",
@@ -104,17 +104,17 @@
104
104
  "debug": "^4.4.3",
105
105
  "embla-carousel-react": "^8.6.0",
106
106
  "eventemitter3": "^5.0.4",
107
- "framer-motion": "^13.0.0",
107
+ "framer-motion": "^13.1.0",
108
108
  "gsap": "^3.15.0",
109
109
  "html-entities": "^2.6.0",
110
- "html2canvas-pro": "^2.3.3",
110
+ "html2canvas-pro": "^2.3.6",
111
111
  "input-otp": "^1.4.2",
112
- "katex": "^0.18.1",
112
+ "katex": "^0.18.4",
113
113
  "lodash": "4.18.1",
114
114
  "moment": "^2.30.1",
115
- "motion": "^13.0.0",
116
- "next": "^16.3.0",
117
- "next-intl": "^4.13.5",
115
+ "motion": "^13.1.0",
116
+ "next": "^16.3.1",
117
+ "next-intl": "^4.13.6",
118
118
  "next-themes": "^0.4.6",
119
119
  "nuqs": "^2.9.5",
120
120
  "prosemirror-state": "^1.4.4",
@@ -124,8 +124,8 @@
124
124
  "react-colorful": "^5.8.0",
125
125
  "react-day-picker": "^10.0.1",
126
126
  "react-dom": "^19.2.8",
127
- "react-dropzone": "^20.0.0",
128
- "react-hook-form": "^7.84.0",
127
+ "react-dropzone": "^20.1.0",
128
+ "react-hook-form": "^7.85.0",
129
129
  "react-markdown": "^10.1.0",
130
130
  "react-papaparse": "^4.4.0",
131
131
  "react-pdf": "^10.4.1",
@@ -133,13 +133,13 @@
133
133
  "react-resizable-panels": "^4.12.2",
134
134
  "react-syntax-highlighter": "^16.1.1",
135
135
  "recharts": "3.10.1",
136
- "slot-text": "^0.3.3",
137
- "sonner": "^2.0.7",
136
+ "slot-text": "^0.3.4",
137
+ "sonner": "^2.0.8",
138
138
  "streamdown": "^2.5.0",
139
139
  "tailwind-scrollbar": "^4.0.2",
140
140
  "tailwind-scrollbar-hide": "^4.0.0",
141
141
  "tailwindcss-animate": "^1.0.7",
142
- "tiptap-extension-resize-image": "^1.4.5",
142
+ "tiptap-extension-resize-image": "^1.4.6",
143
143
  "use-debounce": "^10.1.1",
144
144
  "vaul": "^1.1.2",
145
145
  "y-protocols": "^1.0.7",
@@ -150,14 +150,14 @@
150
150
  "@tailwindcss/postcss": "^4.3.3",
151
151
  "@tailwindcss/typography": "^0.5.20",
152
152
  "@tanstack/react-query": "^5.101.4",
153
- "@tanstack/react-table": "^9.0.0",
154
- "@testing-library/jest-dom": "^7.0.0",
153
+ "@tanstack/react-table": "^9.1.2",
154
+ "@testing-library/jest-dom": "^7.0.1",
155
155
  "@testing-library/react": "^16.3.2",
156
156
  "@tuturuuu/types": "0.27.0",
157
157
  "@tuturuuu/typescript-config": "0.1.1",
158
158
  "@types/html2canvas": "^1.0.0",
159
159
  "@types/lodash": "^4.17.25",
160
- "@types/node": "^26.1.2",
160
+ "@types/node": "^26.2.0",
161
161
  "@types/react": "^19.2.18",
162
162
  "@types/react-dom": "^19.2.4",
163
163
  "@types/react-resizable": "^4.0.0",
@@ -166,7 +166,7 @@
166
166
  "@vitejs/plugin-react": "^6.0.5",
167
167
  "autoprefixer": "^10.5.4",
168
168
  "jsdom": "^30.0.1",
169
- "postcss": "^8.5.25",
169
+ "postcss": "^8.5.26",
170
170
  "postcss-load-config": "^6.0.1",
171
171
  "tailwindcss": "^4.3.3",
172
172
  "typescript": "7.0.2",
@@ -101,6 +101,8 @@ interface ComboboxProps {
101
101
  onCreate?: (value: string) => unknown | Promise<unknown>;
102
102
  /** Callback when search input changes */
103
103
  onSearchChange?: (value: string) => void;
104
+ /** Whether Command should filter the provided options locally */
105
+ shouldFilter?: boolean;
104
106
  /** Callback when the popover opens or closes */
105
107
  onOpenChange?: (open: boolean) => void;
106
108
  /** Whether there are more options to load */
@@ -148,6 +150,7 @@ export function Combobox({
148
150
  useFirstValueAsDefault = false,
149
151
  onCreate,
150
152
  onSearchChange,
153
+ shouldFilter = true,
151
154
  onOpenChange,
152
155
  hasMore = false,
153
156
  onLoadMore,
@@ -543,6 +546,7 @@ export function Combobox({
543
546
  sideOffset={4}
544
547
  >
545
548
  <Command
549
+ shouldFilter={shouldFilter}
546
550
  filter={(value, search) => {
547
551
  if (value.startsWith(actionValuePrefix)) return 1;
548
552
  if (value.startsWith(createValuePrefix)) return 1;
@@ -1,9 +1,21 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { describe, expect, it, vi } from 'vitest';
2
2
  import {
3
3
  createStandardWorkspaceAccessAdapter,
4
+ mergeWorkspaceInvitationRoles,
4
5
  normalizeWorkspaceAccessRole,
5
6
  } from './adapters';
6
7
 
8
+ const mocks = vi.hoisted(() => ({
9
+ inviteWorkspaceMember: vi.fn().mockResolvedValue({ message: 'success' }),
10
+ }));
11
+
12
+ vi.mock('@tuturuuu/internal-api/workspaces', async (importOriginal) => ({
13
+ ...(await importOriginal<
14
+ typeof import('@tuturuuu/internal-api/workspaces')
15
+ >()),
16
+ inviteWorkspaceMember: mocks.inviteWorkspaceMember,
17
+ }));
18
+
7
19
  describe('workspace access adapters', () => {
8
20
  it('normalizes role permissions into the shared access role shape', () => {
9
21
  expect(
@@ -37,4 +49,80 @@ describe('workspace access adapters', () => {
37
49
  createStandardWorkspaceAccessAdapter().updateMemberProfile
38
50
  ).toBeTypeOf('function');
39
51
  });
52
+
53
+ it('hydrates pending email and registered-user invitations with their assigned roles', () => {
54
+ const members = mergeWorkspaceInvitationRoles(
55
+ [
56
+ {
57
+ default_permissions: [],
58
+ email: 'pending@example.com',
59
+ id: null,
60
+ is_creator: false,
61
+ pending: true,
62
+ roles: [],
63
+ },
64
+ {
65
+ default_permissions: [],
66
+ email: null,
67
+ id: 'user-2',
68
+ is_creator: false,
69
+ pending: true,
70
+ roles: [],
71
+ },
72
+ {
73
+ default_permissions: [],
74
+ email: 'joined@example.com',
75
+ id: 'user-3',
76
+ is_creator: false,
77
+ pending: false,
78
+ roles: [{ id: 'role-owner', name: 'Owner', permissions: [] }],
79
+ },
80
+ ],
81
+ [
82
+ {
83
+ email: 'Pending@Example.com',
84
+ roles: [
85
+ { id: 'role-editor', name: 'Editor' },
86
+ { id: 'role-reviewer', name: 'Reviewer' },
87
+ ],
88
+ userId: null,
89
+ },
90
+ {
91
+ email: null,
92
+ roles: [{ id: 'role-reviewer', name: 'Reviewer' }],
93
+ userId: 'user-2',
94
+ },
95
+ ]
96
+ );
97
+
98
+ expect(members[0]?.roles).toEqual([
99
+ { id: 'role-editor', name: 'Editor', permissions: [] },
100
+ { id: 'role-reviewer', name: 'Reviewer', permissions: [] },
101
+ ]);
102
+ expect(members[1]?.roles).toEqual([
103
+ { id: 'role-reviewer', name: 'Reviewer', permissions: [] },
104
+ ]);
105
+ expect(members[2]?.roles).toEqual([
106
+ { id: 'role-owner', name: 'Owner', permissions: [] },
107
+ ]);
108
+ });
109
+
110
+ it('forwards multiple role assignments with every standard invite', async () => {
111
+ const adapter = createStandardWorkspaceAccessAdapter();
112
+
113
+ await adapter.inviteMembers('ws-1', {
114
+ accessPreset: 'member',
115
+ emails: ['editor@example.com'],
116
+ memberType: 'MEMBER',
117
+ roleIds: ['role-editor', 'role-reviewer'],
118
+ });
119
+
120
+ expect(mocks.inviteWorkspaceMember).toHaveBeenCalledWith('ws-1', {
121
+ accessPreset: 'member',
122
+ confirmDefaultAdminMigration: undefined,
123
+ email: 'editor@example.com',
124
+ memberType: 'MEMBER',
125
+ roleIds: ['role-editor', 'role-reviewer'],
126
+ });
127
+ });
40
128
  });
@@ -17,17 +17,24 @@ import {
17
17
  createWorkspaceRole,
18
18
  deleteWorkspaceRole,
19
19
  getWorkspaceDefaultPermissions,
20
+ listWorkspaceRoleOptions,
20
21
  listWorkspaceRoles,
21
22
  updateWorkspaceDefaultPermissions,
22
23
  updateWorkspaceRole,
23
24
  } from '@tuturuuu/internal-api/settings';
25
+ import type { WorkspaceInvitationRoleAssignment } from '@tuturuuu/internal-api/workspaces';
24
26
  import {
25
27
  inviteWorkspaceMember,
26
28
  listEnhancedWorkspaceMembers,
29
+ listWorkspaceInvitationRoles,
27
30
  removeWorkspaceMember,
31
+ updateWorkspaceInvitationRole,
28
32
  updateWorkspaceMemberProfile,
29
33
  } from '@tuturuuu/internal-api/workspaces';
30
- import type { WorkspaceRole } from '@tuturuuu/types';
34
+ import type {
35
+ InternalApiEnhancedWorkspaceMember,
36
+ WorkspaceRole,
37
+ } from '@tuturuuu/types';
31
38
  import type {
32
39
  WorkspaceAccessAdapter,
33
40
  WorkspaceAccessInvitePayload,
@@ -60,6 +67,54 @@ export function normalizeWorkspaceAccessRole(role: RoleLike) {
60
67
  } satisfies WorkspaceAccessRole;
61
68
  }
62
69
 
70
+ export function mergeWorkspaceInvitationRoles(
71
+ members: InternalApiEnhancedWorkspaceMember[],
72
+ invitations: WorkspaceInvitationRoleAssignment[]
73
+ ) {
74
+ const byUserId = new Map(
75
+ invitations
76
+ .filter((invitation) => invitation.userId)
77
+ .map((invitation) => [invitation.userId, invitation])
78
+ );
79
+ const byEmail = new Map(
80
+ invitations
81
+ .filter((invitation) => invitation.email)
82
+ .map((invitation) => [invitation.email?.trim().toLowerCase(), invitation])
83
+ );
84
+
85
+ return members.map((member) => {
86
+ if (!member.pending) return member;
87
+
88
+ const invitation =
89
+ (member.id ? byUserId.get(member.id) : undefined) ??
90
+ (member.email
91
+ ? byEmail.get(member.email.trim().toLowerCase())
92
+ : undefined);
93
+
94
+ return {
95
+ ...member,
96
+ roles: (invitation?.roles ?? []).map((role) => ({
97
+ ...role,
98
+ permissions: [],
99
+ })),
100
+ };
101
+ });
102
+ }
103
+
104
+ async function listStandardWorkspaceMembers(
105
+ workspaceId: string,
106
+ status?: 'all' | 'invited' | 'joined'
107
+ ) {
108
+ const [members, invitations] = await Promise.all([
109
+ listEnhancedWorkspaceMembers(workspaceId, status),
110
+ status === 'joined'
111
+ ? Promise.resolve([])
112
+ : listWorkspaceInvitationRoles(workspaceId),
113
+ ]);
114
+
115
+ return mergeWorkspaceInvitationRoles(members, invitations);
116
+ }
117
+
63
118
  async function inviteStandardWorkspaceMembers(
64
119
  workspaceId: string,
65
120
  payload: WorkspaceAccessInvitePayload
@@ -74,6 +129,7 @@ async function inviteStandardWorkspaceMembers(
74
129
  confirmDefaultAdminMigration: payload.confirmDefaultAdminMigration,
75
130
  email,
76
131
  memberType: payload.memberType,
132
+ roleIds: payload.roleIds,
77
133
  };
78
134
  return inviteWorkspaceMember(workspaceId, invitePayload);
79
135
  })
@@ -132,7 +188,8 @@ export function createStandardWorkspaceAccessAdapter(): WorkspaceAccessAdapter {
132
188
  return role;
133
189
  },
134
190
  inviteMembers: inviteStandardWorkspaceMembers,
135
- listMembers: listEnhancedWorkspaceMembers,
191
+ listMembers: listStandardWorkspaceMembers,
192
+ listRoleOptions: listWorkspaceRoleOptions,
136
193
  listRoles: async (workspaceId, query) => {
137
194
  const result = await listWorkspaceRoles(workspaceId, {
138
195
  page: query?.page ?? '1',
@@ -148,6 +205,7 @@ export function createStandardWorkspaceAccessAdapter(): WorkspaceAccessAdapter {
148
205
  removeMember: removeWorkspaceMember,
149
206
  removeRoleMember,
150
207
  updateMemberProfile: updateWorkspaceMemberProfile,
208
+ updateInvitationRole: updateWorkspaceInvitationRole,
151
209
  updateDefaultRole: (workspaceId, memberType, payload) =>
152
210
  updateWorkspaceDefaultPermissions(workspaceId, memberType, {
153
211
  permissions: payload.permissions as WorkspaceRole['permissions'],
@@ -170,6 +228,13 @@ export function createExternalProjectWorkspaceAccessAdapter(): WorkspaceAccessAd
170
228
  inviteMembers: (workspaceId, payload) =>
171
229
  inviteWorkspaceExternalProjectMembers(workspaceId, payload.emails),
172
230
  listMembers: listWorkspaceExternalProjectMembers,
231
+ listRoleOptions: async (workspaceId) => {
232
+ const roles = await listWorkspaceExternalProjectRoles(workspaceId);
233
+ return {
234
+ count: roles.length,
235
+ data: roles.map(({ id, name }) => ({ id, name })),
236
+ };
237
+ },
173
238
  listRoles: async (workspaceId, query) => {
174
239
  const roles = (await listWorkspaceExternalProjectRoles(workspaceId)).map(
175
240
  normalizeWorkspaceAccessRole
@@ -56,6 +56,7 @@ export type WorkspaceAccessInvitePayload = {
56
56
  confirmDefaultAdminMigration?: boolean;
57
57
  emails: string[];
58
58
  memberType: WorkspaceDefaultPermissionMemberType;
59
+ roleIds: string[];
59
60
  };
60
61
 
61
62
  export type WorkspaceAccessAdapter = {
@@ -91,6 +92,10 @@ export type WorkspaceAccessAdapter = {
91
92
  workspaceId: string,
92
93
  status?: WorkspaceAccessMemberStatus
93
94
  ) => Promise<InternalApiEnhancedWorkspaceMember[]>;
95
+ listRoleOptions: (
96
+ workspaceId: string,
97
+ query?: { page?: string; pageSize?: string }
98
+ ) => Promise<{ count: number; data: Array<{ id: string; name: string }> }>;
94
99
  listRoles: (
95
100
  workspaceId: string,
96
101
  query?: { page?: string; pageSize?: string; q?: string }
@@ -112,6 +117,14 @@ export type WorkspaceAccessAdapter = {
112
117
  userId?: null | string;
113
118
  }
114
119
  ) => Promise<unknown>;
120
+ updateInvitationRole?: (
121
+ workspaceId: string,
122
+ payload: {
123
+ email?: null | string;
124
+ roleIds: string[];
125
+ userId?: null | string;
126
+ }
127
+ ) => Promise<unknown>;
115
128
  updateDefaultRole: (
116
129
  workspaceId: string,
117
130
  memberType: WorkspaceDefaultPermissionMemberType,
@@ -0,0 +1,79 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { WorkspaceAccessInvitationRoleMenu } from './workspace-access-invitation-role-menu';
4
+
5
+ vi.mock('next-intl', () => ({
6
+ useTranslations: () => (key: string, values?: Record<string, number>) =>
7
+ values?.count === undefined ? key : `${values.count} ${key}`,
8
+ }));
9
+
10
+ describe('WorkspaceAccessInvitationRoleMenu', () => {
11
+ it('presents an actionable empty state and adds roles to an email invite', async () => {
12
+ const onUpdate = vi.fn();
13
+ render(
14
+ <WorkspaceAccessInvitationRoleMenu
15
+ email="pending@example.com"
16
+ isMutating={false}
17
+ onUpdate={onUpdate}
18
+ assignedRoles={[]}
19
+ roles={[
20
+ { id: 'role-editor', name: 'Editor' },
21
+ { id: 'role-reviewer', name: 'Reviewer' },
22
+ ]}
23
+ />
24
+ );
25
+
26
+ expect(
27
+ screen.getByRole('button', {
28
+ name: /ws-members.assign_invitation_role/i,
29
+ })
30
+ ).toBeDefined();
31
+ expect(screen.getByText('ws-members.invitation_role_helper')).toBeDefined();
32
+
33
+ fireEvent.pointerDown(
34
+ screen.getByRole('button', {
35
+ name: /ws-members.assign_invitation_role/i,
36
+ }),
37
+ { button: 0, ctrlKey: false }
38
+ );
39
+ fireEvent.click(await screen.findByText('Editor'));
40
+
41
+ expect(onUpdate).toHaveBeenCalledWith({
42
+ email: 'pending@example.com',
43
+ roleIds: ['role-editor'],
44
+ userId: undefined,
45
+ });
46
+ });
47
+
48
+ it('shows all assigned roles and removes only the selected role', async () => {
49
+ const onUpdate = vi.fn();
50
+ render(
51
+ <WorkspaceAccessInvitationRoleMenu
52
+ isMutating={false}
53
+ onUpdate={onUpdate}
54
+ assignedRoles={[
55
+ { id: 'role-editor', name: 'Editor' },
56
+ { id: 'role-reviewer', name: 'Reviewer' },
57
+ ]}
58
+ roles={[
59
+ { id: 'role-editor', name: 'Editor' },
60
+ { id: 'role-reviewer', name: 'Reviewer' },
61
+ ]}
62
+ userId="user-2"
63
+ />
64
+ );
65
+
66
+ fireEvent.pointerDown(
67
+ screen.getByRole('button', { name: /2 ws-members.roles_selected/i }),
68
+ { button: 0, ctrlKey: false }
69
+ );
70
+ const reviewerLabels = await screen.findAllByText('Reviewer');
71
+ fireEvent.click(reviewerLabels.at(-1)!);
72
+
73
+ expect(onUpdate).toHaveBeenCalledWith({
74
+ email: undefined,
75
+ roleIds: ['role-editor'],
76
+ userId: 'user-2',
77
+ });
78
+ });
79
+ });