@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.
- package/CHANGELOG.md +23 -0
- package/biome.json +1 -1
- package/package.json +48 -48
- package/src/components/ui/custom/combobox.tsx +4 -0
- package/src/components/ui/custom/workspace-access/adapters.test.ts +89 -1
- package/src/components/ui/custom/workspace-access/adapters.ts +67 -2
- package/src/components/ui/custom/workspace-access/types.ts +13 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.test.tsx +79 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.tsx +125 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-access-picker.tsx +108 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.test.tsx +134 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.tsx +94 -113
- package/src/components/ui/custom/workspace-access/workspace-access-invite-pos-panel.tsx +75 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-role-picker.tsx +151 -0
- package/src/components/ui/custom/workspace-access/workspace-access-labels.ts +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +27 -2
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +10 -0
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +102 -3
- package/src/components/ui/custom/workspace-access/workspace-access-role-options.test.ts +35 -0
- package/src/components/ui/custom/workspace-access/workspace-access-role-options.ts +21 -0
- package/src/components/ui/custom/workspace-select-invitations.tsx +2 -1
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -1
- package/src/hooks/use-board-actions.test.ts +23 -0
- package/src/hooks/use-board-actions.ts +48 -35
- package/src/hooks/use-calendar-sync.tsx +12 -12
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
64
|
-
"@tiptap/extension-collaboration": "3.
|
|
65
|
-
"@tiptap/extension-collaboration-caret": "3.
|
|
66
|
-
"@tiptap/extension-color": "3.
|
|
67
|
-
"@tiptap/extension-drag-handle": "3.
|
|
68
|
-
"@tiptap/extension-drag-handle-react": "3.
|
|
69
|
-
"@tiptap/extension-highlight": "3.
|
|
70
|
-
"@tiptap/extension-horizontal-rule": "3.
|
|
71
|
-
"@tiptap/extension-image": "3.
|
|
72
|
-
"@tiptap/extension-link": "3.
|
|
73
|
-
"@tiptap/extension-list": "3.
|
|
74
|
-
"@tiptap/extension-node-range": "3.
|
|
75
|
-
"@tiptap/extension-placeholder": "3.
|
|
76
|
-
"@tiptap/extension-strike": "3.
|
|
77
|
-
"@tiptap/extension-subscript": "3.
|
|
78
|
-
"@tiptap/extension-superscript": "3.
|
|
79
|
-
"@tiptap/extension-table": "3.
|
|
80
|
-
"@tiptap/extension-table-cell": "3.
|
|
81
|
-
"@tiptap/extension-table-header": "3.
|
|
82
|
-
"@tiptap/extension-table-row": "3.
|
|
83
|
-
"@tiptap/extension-text-align": "3.
|
|
84
|
-
"@tiptap/extension-text-style": "3.
|
|
85
|
-
"@tiptap/extension-youtube": "3.
|
|
86
|
-
"@tiptap/pm": "3.
|
|
87
|
-
"@tiptap/react": "3.
|
|
88
|
-
"@tiptap/starter-kit": "3.
|
|
89
|
-
"@tuturuuu/ai": "0.8.
|
|
90
|
-
"@tuturuuu/apis": "0.11.
|
|
91
|
-
"@tuturuuu/hooks": "0.1.
|
|
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.
|
|
93
|
+
"@tuturuuu/internal-api": "0.30.0",
|
|
94
94
|
"@tuturuuu/supabase": "0.5.0",
|
|
95
|
-
"@tuturuuu/utils": "0.
|
|
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.
|
|
107
|
+
"framer-motion": "^13.1.0",
|
|
108
108
|
"gsap": "^3.15.0",
|
|
109
109
|
"html-entities": "^2.6.0",
|
|
110
|
-
"html2canvas-pro": "^2.3.
|
|
110
|
+
"html2canvas-pro": "^2.3.6",
|
|
111
111
|
"input-otp": "^1.4.2",
|
|
112
|
-
"katex": "^0.18.
|
|
112
|
+
"katex": "^0.18.4",
|
|
113
113
|
"lodash": "4.18.1",
|
|
114
114
|
"moment": "^2.30.1",
|
|
115
|
-
"motion": "^13.
|
|
116
|
-
"next": "^16.3.
|
|
117
|
-
"next-intl": "^4.13.
|
|
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.
|
|
128
|
-
"react-hook-form": "^7.
|
|
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.
|
|
137
|
-
"sonner": "^2.0.
|
|
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.
|
|
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.
|
|
154
|
-
"@testing-library/jest-dom": "^7.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.
|
|
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.
|
|
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 {
|
|
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:
|
|
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,
|
package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.test.tsx
ADDED
|
@@ -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
|
+
});
|