@tuturuuu/ui 0.26.1 → 0.28.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 +37 -0
- package/biome.json +1 -1
- package/package.json +49 -48
- package/src/components/ui/button.tsx +12 -8
- package/src/components/ui/color-picker.test.tsx +23 -0
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/custom/combobox.tsx +4 -0
- package/src/components/ui/custom/notification-popover-client.tsx +6 -19
- package/src/components/ui/custom/notification-popover-trigger.test.tsx +37 -0
- package/src/components/ui/custom/notification-popover-trigger.tsx +44 -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/components/ui/text-editor/__tests__/editor-classes.test.ts +36 -0
- package/src/components/ui/text-editor/__tests__/inline-task-conversion.test.tsx +30 -0
- package/src/components/ui/text-editor/__tests__/keyboard.test.ts +173 -3
- package/src/components/ui/text-editor/__tests__/toggle-block-extension.test.ts +251 -0
- package/src/components/ui/text-editor/editor-classes.ts +100 -0
- package/src/components/ui/text-editor/editor.tsx +11 -153
- package/src/components/ui/text-editor/extensions.ts +19 -0
- package/src/components/ui/text-editor/keyboard.ts +47 -0
- package/src/components/ui/text-editor/toggle-block-extension.ts +153 -0
- package/src/components/ui/text-editor/tool-bar.tsx +24 -73
- package/src/components/ui/text-editor/toolbar-config.ts +67 -0
- 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
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
CreditCard,
|
|
5
|
-
MailPlus,
|
|
6
|
-
ShieldCheck,
|
|
7
|
-
TriangleAlert,
|
|
8
|
-
UserPlus,
|
|
9
|
-
} from '@tuturuuu/icons';
|
|
3
|
+
import { MailPlus, UserPlus } from '@tuturuuu/icons';
|
|
10
4
|
import { Badge } from '@tuturuuu/ui/badge';
|
|
11
5
|
import { Button } from '@tuturuuu/ui/button';
|
|
12
|
-
import { Checkbox } from '@tuturuuu/ui/checkbox';
|
|
13
6
|
import {
|
|
14
7
|
Dialog,
|
|
15
8
|
DialogContent,
|
|
@@ -19,16 +12,12 @@ import {
|
|
|
19
12
|
DialogTitle,
|
|
20
13
|
} from '@tuturuuu/ui/dialog';
|
|
21
14
|
import { Label } from '@tuturuuu/ui/label';
|
|
22
|
-
import {
|
|
23
|
-
Select,
|
|
24
|
-
SelectContent,
|
|
25
|
-
SelectItem,
|
|
26
|
-
SelectTrigger,
|
|
27
|
-
SelectValue,
|
|
28
|
-
} from '@tuturuuu/ui/select';
|
|
29
15
|
import { Textarea } from '@tuturuuu/ui/textarea';
|
|
30
16
|
import { useTranslations } from 'next-intl';
|
|
31
17
|
import { parseInviteEmails } from './member-filter-utils';
|
|
18
|
+
import { WorkspaceAccessInviteAccessPicker } from './workspace-access-invite-access-picker';
|
|
19
|
+
import { WorkspaceAccessInvitePosPanel } from './workspace-access-invite-pos-panel';
|
|
20
|
+
import { WorkspaceAccessInviteRolePicker } from './workspace-access-invite-role-picker';
|
|
32
21
|
|
|
33
22
|
type Props = {
|
|
34
23
|
accessPreset: 'guest' | 'member' | 'pos_operator';
|
|
@@ -38,12 +27,16 @@ type Props = {
|
|
|
38
27
|
emails: string;
|
|
39
28
|
isSubmitting: boolean;
|
|
40
29
|
joinedMemberCount: number;
|
|
30
|
+
noRoleLabel: string;
|
|
41
31
|
onAccessPresetChange: (value: 'guest' | 'member' | 'pos_operator') => void;
|
|
42
32
|
onConfirmDefaultAdminMigrationChange: (value: boolean) => void;
|
|
43
33
|
onEmailsChange: (value: string) => void;
|
|
44
34
|
onOpenChange: (open: boolean) => void;
|
|
35
|
+
onRoleIdsChange: (roleIds: string[]) => void;
|
|
45
36
|
onSubmit: () => void;
|
|
46
37
|
open: boolean;
|
|
38
|
+
roleIds: string[];
|
|
39
|
+
roles: Array<{ id: string; name: string }>;
|
|
47
40
|
};
|
|
48
41
|
|
|
49
42
|
export function WorkspaceAccessInviteDialog({
|
|
@@ -54,19 +47,32 @@ export function WorkspaceAccessInviteDialog({
|
|
|
54
47
|
emails,
|
|
55
48
|
isSubmitting,
|
|
56
49
|
joinedMemberCount,
|
|
50
|
+
noRoleLabel,
|
|
57
51
|
onAccessPresetChange,
|
|
58
52
|
onConfirmDefaultAdminMigrationChange,
|
|
59
53
|
onEmailsChange,
|
|
60
54
|
onOpenChange,
|
|
55
|
+
onRoleIdsChange,
|
|
61
56
|
onSubmit,
|
|
62
57
|
open,
|
|
58
|
+
roleIds,
|
|
59
|
+
roles,
|
|
63
60
|
}: Props) {
|
|
64
|
-
const t = useTranslations()
|
|
61
|
+
const t = useTranslations();
|
|
65
62
|
const count = parseInviteEmails(emails).length;
|
|
63
|
+
const selectedRoleNames = roles
|
|
64
|
+
.filter((role) => roleIds.includes(role.id))
|
|
65
|
+
.map((role) => role.name);
|
|
66
|
+
const accessSummary =
|
|
67
|
+
accessPreset === 'member'
|
|
68
|
+
? selectedRoleNames.length > 0
|
|
69
|
+
? `${t('ws-members.invite_access_member_tab')} · ${selectedRoleNames.join(', ')}`
|
|
70
|
+
: `${t('ws-members.invite_access_member_tab')} · ${t('ws-members.no_roles_assigned')}`
|
|
71
|
+
: t(`ws-members.invite_membership_${accessPreset}`);
|
|
66
72
|
|
|
67
73
|
return (
|
|
68
74
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
69
|
-
<DialogContent className="flex max-h-[calc(100dvh-1rem)] w-[calc(100%-1rem)] max-w-none flex-col gap-0 overflow-hidden rounded-b-none p-0 max-sm:top-auto max-sm:bottom-0 max-sm:left-0 max-sm:translate-x-0 max-sm:translate-y-0 sm:max-h-[min(
|
|
75
|
+
<DialogContent className="flex max-h-[calc(100dvh-1rem)] w-[calc(100%-1rem)] max-w-none flex-col gap-0 overflow-hidden rounded-b-none p-0 max-sm:top-auto max-sm:bottom-0 max-sm:left-0 max-sm:translate-x-0 max-sm:translate-y-0 sm:max-h-[min(90dvh,54rem)] sm:max-w-4xl sm:rounded-2xl">
|
|
70
76
|
<DialogHeader className="shrink-0 gap-0 border-b p-4 pr-12 text-left sm:p-6 sm:pr-12">
|
|
71
77
|
<div className="flex items-start gap-3">
|
|
72
78
|
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl border border-dynamic-blue/25 bg-dynamic-blue/10 text-dynamic-blue">
|
|
@@ -81,114 +87,89 @@ export function WorkspaceAccessInviteDialog({
|
|
|
81
87
|
</div>
|
|
82
88
|
</DialogHeader>
|
|
83
89
|
|
|
84
|
-
<div className="min-h-0 flex-1
|
|
85
|
-
<div className="grid
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
className="
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
id="workspace-access-invite-membership"
|
|
113
|
-
className="w-full"
|
|
114
|
-
>
|
|
115
|
-
<SelectValue />
|
|
116
|
-
</SelectTrigger>
|
|
117
|
-
<SelectContent>
|
|
118
|
-
<SelectItem value="member">
|
|
119
|
-
{t('ws-members.invite_membership_member')}
|
|
120
|
-
</SelectItem>
|
|
121
|
-
<SelectItem value="guest">
|
|
122
|
-
{t('ws-members.invite_membership_guest')}
|
|
123
|
-
</SelectItem>
|
|
124
|
-
<SelectItem value="pos_operator" disabled={!canManageRoles}>
|
|
125
|
-
{t('ws-members.invite_membership_pos_operator')}
|
|
126
|
-
</SelectItem>
|
|
127
|
-
</SelectContent>
|
|
128
|
-
</Select>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
{accessPreset === 'pos_operator' ? (
|
|
132
|
-
<div className="space-y-3 rounded-xl border border-dynamic-blue/25 bg-dynamic-blue/5 p-3.5 sm:p-4">
|
|
133
|
-
<div className="flex items-start gap-3">
|
|
134
|
-
<div className="mt-0.5 rounded-lg border border-dynamic-blue/20 bg-background p-2 text-dynamic-blue">
|
|
135
|
-
<CreditCard className="h-4 w-4" />
|
|
136
|
-
</div>
|
|
137
|
-
<div className="min-w-0 space-y-1">
|
|
138
|
-
<p className="font-medium text-sm">
|
|
139
|
-
{t('ws-members.pos_operator_title')}
|
|
140
|
-
</p>
|
|
141
|
-
<p className="text-muted-foreground text-sm leading-5">
|
|
142
|
-
{t('ws-members.pos_operator_description')}
|
|
143
|
-
</p>
|
|
90
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
91
|
+
<div className="grid lg:grid-cols-[minmax(0,1fr)_minmax(22rem,1fr)] lg:divide-x">
|
|
92
|
+
<section className="space-y-4 p-4 sm:p-6">
|
|
93
|
+
<div className="flex items-center gap-2">
|
|
94
|
+
<span className="flex size-6 items-center justify-center rounded-full bg-foreground font-semibold text-background text-xs">
|
|
95
|
+
1
|
|
96
|
+
</span>
|
|
97
|
+
<h3 className="font-semibold text-sm">
|
|
98
|
+
{t('ws-members.invite_recipients_title')}
|
|
99
|
+
</h3>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="grid gap-2">
|
|
102
|
+
<Label htmlFor="workspace-access-invite-emails">
|
|
103
|
+
{t('ws-members.invite_emails_label')}
|
|
104
|
+
</Label>
|
|
105
|
+
<Textarea
|
|
106
|
+
id="workspace-access-invite-emails"
|
|
107
|
+
rows={7}
|
|
108
|
+
value={emails}
|
|
109
|
+
onChange={(event) => onEmailsChange(event.target.value)}
|
|
110
|
+
placeholder={'one@example.com\ntwo@example.com'}
|
|
111
|
+
className="min-h-40 resize-y rounded-xl bg-muted/20 font-mono text-sm leading-6"
|
|
112
|
+
/>
|
|
113
|
+
<div className="flex items-center justify-between gap-3 text-muted-foreground text-xs">
|
|
114
|
+
<span>{t('ws-members.invite_emails_helper')}</span>
|
|
115
|
+
<Badge variant="outline" className="rounded-full">
|
|
116
|
+
{t('ws-members.recipients_count', { count })}
|
|
117
|
+
</Badge>
|
|
144
118
|
</div>
|
|
145
119
|
</div>
|
|
120
|
+
</section>
|
|
146
121
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
</div>
|
|
156
|
-
) : null}
|
|
157
|
-
<div className="flex items-center gap-2 rounded-lg border bg-background/70 p-3 text-sm">
|
|
158
|
-
<CreditCard className="h-4 w-4 text-dynamic-blue" />
|
|
159
|
-
<span>{t('ws-members.pos_operator_only_permission')}</span>
|
|
160
|
-
</div>
|
|
122
|
+
<section className="space-y-5 bg-muted/10 p-4 sm:p-6">
|
|
123
|
+
<div className="flex items-center gap-2">
|
|
124
|
+
<span className="flex size-6 items-center justify-center rounded-full bg-foreground font-semibold text-background text-xs">
|
|
125
|
+
2
|
|
126
|
+
</span>
|
|
127
|
+
<h3 className="font-semibold text-sm">
|
|
128
|
+
{t('ws-members.invite_access_title')}
|
|
129
|
+
</h3>
|
|
161
130
|
</div>
|
|
162
131
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
132
|
+
<WorkspaceAccessInviteAccessPicker
|
|
133
|
+
canManageRoles={canManageRoles}
|
|
134
|
+
onChange={onAccessPresetChange}
|
|
135
|
+
value={accessPreset}
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
{canManageRoles && accessPreset === 'member' ? (
|
|
139
|
+
<WorkspaceAccessInviteRolePicker
|
|
140
|
+
noRoleLabel={noRoleLabel}
|
|
141
|
+
onChange={onRoleIdsChange}
|
|
142
|
+
roleIds={roleIds}
|
|
143
|
+
roles={roles}
|
|
144
|
+
/>
|
|
170
145
|
) : null}
|
|
171
146
|
|
|
172
|
-
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
147
|
+
{accessPreset === 'pos_operator' ? (
|
|
148
|
+
<WorkspaceAccessInvitePosPanel
|
|
149
|
+
confirmDefaultAdminMigration={confirmDefaultAdminMigration}
|
|
150
|
+
defaultAdminEnabled={defaultAdminEnabled}
|
|
151
|
+
joinedMemberCount={joinedMemberCount}
|
|
152
|
+
onConfirmDefaultAdminMigrationChange={
|
|
153
|
+
onConfirmDefaultAdminMigrationChange
|
|
178
154
|
}
|
|
179
155
|
/>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
</label>
|
|
184
|
-
</div>
|
|
185
|
-
) : null}
|
|
156
|
+
) : null}
|
|
157
|
+
</section>
|
|
158
|
+
</div>
|
|
186
159
|
</div>
|
|
187
160
|
|
|
188
161
|
<DialogFooter className="grid shrink-0 grid-cols-[auto_1fr] items-center gap-3 border-t bg-muted/20 p-3 sm:flex sm:p-4">
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
162
|
+
<div className="min-w-0">
|
|
163
|
+
<p className="font-medium text-sm">
|
|
164
|
+
{t('ws-members.recipients_count', { count })}
|
|
165
|
+
</p>
|
|
166
|
+
<p
|
|
167
|
+
className="truncate text-muted-foreground text-xs"
|
|
168
|
+
title={accessSummary}
|
|
169
|
+
>
|
|
170
|
+
{accessSummary}
|
|
171
|
+
</p>
|
|
172
|
+
</div>
|
|
192
173
|
<Button
|
|
193
174
|
className="min-w-0"
|
|
194
175
|
disabled={
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CreditCard, ShieldCheck, TriangleAlert } from '@tuturuuu/icons';
|
|
4
|
+
import { Checkbox } from '@tuturuuu/ui/checkbox';
|
|
5
|
+
import { useTranslations } from 'next-intl';
|
|
6
|
+
|
|
7
|
+
export function WorkspaceAccessInvitePosPanel({
|
|
8
|
+
confirmDefaultAdminMigration,
|
|
9
|
+
defaultAdminEnabled,
|
|
10
|
+
joinedMemberCount,
|
|
11
|
+
onConfirmDefaultAdminMigrationChange,
|
|
12
|
+
}: {
|
|
13
|
+
confirmDefaultAdminMigration: boolean;
|
|
14
|
+
defaultAdminEnabled: boolean;
|
|
15
|
+
joinedMemberCount: number;
|
|
16
|
+
onConfirmDefaultAdminMigrationChange: (value: boolean) => void;
|
|
17
|
+
}) {
|
|
18
|
+
const t = useTranslations();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="space-y-3 rounded-xl border border-dynamic-blue/25 bg-dynamic-blue/5 p-3.5 sm:p-4">
|
|
22
|
+
<div className="flex items-start gap-3">
|
|
23
|
+
<div className="mt-0.5 rounded-lg border border-dynamic-blue/20 bg-background p-2 text-dynamic-blue">
|
|
24
|
+
<CreditCard className="size-4" />
|
|
25
|
+
</div>
|
|
26
|
+
<div className="min-w-0 space-y-1">
|
|
27
|
+
<p className="font-medium text-sm">
|
|
28
|
+
{t('ws-members.pos_operator_title')}
|
|
29
|
+
</p>
|
|
30
|
+
<p className="text-muted-foreground text-sm leading-5">
|
|
31
|
+
{t('ws-members.pos_operator_description')}
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
37
|
+
{defaultAdminEnabled ? (
|
|
38
|
+
<div className="flex items-center gap-2 rounded-lg border bg-background/70 p-3 text-sm">
|
|
39
|
+
<ShieldCheck className="size-4 text-dynamic-green" />
|
|
40
|
+
<span>
|
|
41
|
+
{joinedMemberCount}{' '}
|
|
42
|
+
{t('ws-members.pos_operator_existing_members')}
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
) : null}
|
|
46
|
+
<div className="flex items-center gap-2 rounded-lg border bg-background/70 p-3 text-sm">
|
|
47
|
+
<CreditCard className="size-4 text-dynamic-blue" />
|
|
48
|
+
<span>{t('ws-members.pos_operator_only_permission')}</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{defaultAdminEnabled ? (
|
|
53
|
+
<div className="flex items-start gap-2 rounded-lg border border-dynamic-orange/25 bg-dynamic-orange/5 p-3 text-sm">
|
|
54
|
+
<TriangleAlert className="mt-0.5 size-4 shrink-0 text-dynamic-orange" />
|
|
55
|
+
<p className="text-muted-foreground leading-5">
|
|
56
|
+
{t('ws-members.pos_operator_admin_migration_note')}
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
) : null}
|
|
60
|
+
|
|
61
|
+
<label className="flex cursor-pointer items-start gap-3 rounded-lg border bg-background p-3 transition-colors hover:bg-muted/20 active:scale-[0.99]">
|
|
62
|
+
<Checkbox
|
|
63
|
+
checked={confirmDefaultAdminMigration}
|
|
64
|
+
className="mt-0.5"
|
|
65
|
+
onCheckedChange={(checked) =>
|
|
66
|
+
onConfirmDefaultAdminMigrationChange(checked === true)
|
|
67
|
+
}
|
|
68
|
+
/>
|
|
69
|
+
<span className="text-sm leading-5">
|
|
70
|
+
{t('ws-members.pos_operator_confirmation')}
|
|
71
|
+
</span>
|
|
72
|
+
</label>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Search, ShieldUser } from '@tuturuuu/icons';
|
|
4
|
+
import {
|
|
5
|
+
Accordion,
|
|
6
|
+
AccordionContent,
|
|
7
|
+
AccordionItem,
|
|
8
|
+
AccordionTrigger,
|
|
9
|
+
} from '@tuturuuu/ui/accordion';
|
|
10
|
+
import { Badge } from '@tuturuuu/ui/badge';
|
|
11
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
12
|
+
import { Checkbox } from '@tuturuuu/ui/checkbox';
|
|
13
|
+
import { Input } from '@tuturuuu/ui/input';
|
|
14
|
+
import { Label } from '@tuturuuu/ui/label';
|
|
15
|
+
import { useTranslations } from 'next-intl';
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
|
|
18
|
+
export function WorkspaceAccessInviteRolePicker({
|
|
19
|
+
noRoleLabel,
|
|
20
|
+
onChange,
|
|
21
|
+
roleIds,
|
|
22
|
+
roles,
|
|
23
|
+
}: {
|
|
24
|
+
noRoleLabel: string;
|
|
25
|
+
onChange: (roleIds: string[]) => void;
|
|
26
|
+
roleIds: string[];
|
|
27
|
+
roles: Array<{ id: string; name: string }>;
|
|
28
|
+
}) {
|
|
29
|
+
const t = useTranslations();
|
|
30
|
+
const [query, setQuery] = useState('');
|
|
31
|
+
const selected = new Set(roleIds);
|
|
32
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
33
|
+
const visibleRoles = normalizedQuery
|
|
34
|
+
? roles.filter((role) =>
|
|
35
|
+
role.name.toLocaleLowerCase().includes(normalizedQuery)
|
|
36
|
+
)
|
|
37
|
+
: roles;
|
|
38
|
+
|
|
39
|
+
const toggleRole = (roleId: string) => {
|
|
40
|
+
onChange(
|
|
41
|
+
selected.has(roleId)
|
|
42
|
+
? roleIds.filter((id) => id !== roleId)
|
|
43
|
+
: [...roleIds, roleId]
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Accordion type="single" collapsible defaultValue="workspace-roles">
|
|
49
|
+
<AccordionItem
|
|
50
|
+
value="workspace-roles"
|
|
51
|
+
className="overflow-hidden rounded-xl border bg-background px-3"
|
|
52
|
+
>
|
|
53
|
+
<AccordionTrigger className="items-center py-3 hover:no-underline">
|
|
54
|
+
<span className="flex min-w-0 items-center gap-3">
|
|
55
|
+
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-muted/30 text-dynamic-blue">
|
|
56
|
+
<ShieldUser className="size-4" />
|
|
57
|
+
</span>
|
|
58
|
+
<span className="min-w-0 text-left">
|
|
59
|
+
<span className="flex items-center gap-2">
|
|
60
|
+
<span className="truncate">
|
|
61
|
+
{t('ws-members.role-placeholder')}
|
|
62
|
+
</span>
|
|
63
|
+
<Badge
|
|
64
|
+
variant="outline"
|
|
65
|
+
className="rounded-full font-normal text-[10px] uppercase tracking-wide"
|
|
66
|
+
>
|
|
67
|
+
{t('ws-members.invite_roles_optional')}
|
|
68
|
+
</Badge>
|
|
69
|
+
</span>
|
|
70
|
+
<span
|
|
71
|
+
aria-live="polite"
|
|
72
|
+
className="mt-0.5 block text-muted-foreground text-xs"
|
|
73
|
+
>
|
|
74
|
+
{t('ws-members.roles_selected', { count: roleIds.length })}
|
|
75
|
+
</span>
|
|
76
|
+
</span>
|
|
77
|
+
</span>
|
|
78
|
+
</AccordionTrigger>
|
|
79
|
+
|
|
80
|
+
<AccordionContent className="space-y-3 border-t pt-3">
|
|
81
|
+
<p className="text-muted-foreground text-xs leading-5">
|
|
82
|
+
{t('ws-members.invite_roles_description')}
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
{roles.length > 0 ? (
|
|
86
|
+
<>
|
|
87
|
+
<div className="flex items-center gap-2">
|
|
88
|
+
<div className="relative min-w-0 flex-1">
|
|
89
|
+
<Label
|
|
90
|
+
htmlFor="workspace-access-invite-role-search"
|
|
91
|
+
className="sr-only"
|
|
92
|
+
>
|
|
93
|
+
{t('ws-members.invite_roles_search')}
|
|
94
|
+
</Label>
|
|
95
|
+
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
96
|
+
<Input
|
|
97
|
+
id="workspace-access-invite-role-search"
|
|
98
|
+
value={query}
|
|
99
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
100
|
+
placeholder={t('ws-members.invite_roles_search')}
|
|
101
|
+
className="h-10 rounded-lg pl-9"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
{roleIds.length > 0 ? (
|
|
105
|
+
<Button
|
|
106
|
+
type="button"
|
|
107
|
+
variant="ghost"
|
|
108
|
+
size="sm"
|
|
109
|
+
className="shrink-0"
|
|
110
|
+
onClick={() => onChange([])}
|
|
111
|
+
>
|
|
112
|
+
{t('ws-members.clear_all_roles')}
|
|
113
|
+
</Button>
|
|
114
|
+
) : null}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{visibleRoles.length > 0 ? (
|
|
118
|
+
<div className="max-h-52 space-y-1 overflow-y-auto rounded-lg border p-1.5">
|
|
119
|
+
{visibleRoles.map((role) => (
|
|
120
|
+
<label
|
|
121
|
+
key={role.id}
|
|
122
|
+
className={`flex cursor-pointer items-center gap-3 rounded-md px-3 py-2.5 transition-colors active:scale-[0.99] ${selected.has(role.id) ? 'bg-dynamic-blue/10 text-foreground' : 'hover:bg-muted/50'}`}
|
|
123
|
+
>
|
|
124
|
+
<Checkbox
|
|
125
|
+
aria-label={role.name}
|
|
126
|
+
checked={selected.has(role.id)}
|
|
127
|
+
onCheckedChange={() => toggleRole(role.id)}
|
|
128
|
+
/>
|
|
129
|
+
<ShieldUser className="size-4 text-dynamic-blue" />
|
|
130
|
+
<span className="min-w-0 flex-1 truncate font-medium text-sm">
|
|
131
|
+
{role.name}
|
|
132
|
+
</span>
|
|
133
|
+
</label>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
) : (
|
|
137
|
+
<div className="rounded-lg border border-dashed p-4 text-center text-muted-foreground text-sm">
|
|
138
|
+
{t('ws-members.invite_roles_no_match')}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</>
|
|
142
|
+
) : (
|
|
143
|
+
<div className="rounded-lg border border-dashed p-4 text-center text-muted-foreground text-sm">
|
|
144
|
+
{noRoleLabel}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</AccordionContent>
|
|
148
|
+
</AccordionItem>
|
|
149
|
+
</Accordion>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -32,7 +32,7 @@ export function getWorkspaceAccessLabels(
|
|
|
32
32
|
noRolesLabel:
|
|
33
33
|
mode === 'cms'
|
|
34
34
|
? t('external-projects.settings.no_roles_label')
|
|
35
|
-
: t('ws-members.
|
|
35
|
+
: t('ws-members.no_roles_assigned'),
|
|
36
36
|
protectedMemberLabel:
|
|
37
37
|
mode === 'cms'
|
|
38
38
|
? t('external-projects.settings.protected_member_label')
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
shouldShowProtectedMemberStatus,
|
|
35
35
|
} from './member-filter-utils';
|
|
36
36
|
import type { WorkspaceAccessLabels, WorkspaceAccessRole } from './types';
|
|
37
|
+
import { WorkspaceAccessInvitationRoleMenu } from './workspace-access-invitation-role-menu';
|
|
37
38
|
|
|
38
39
|
type GuestContext = InternalApiEnhancedWorkspaceMember & {
|
|
39
40
|
direct_board_guest?: boolean;
|
|
@@ -46,6 +47,7 @@ type Props = {
|
|
|
46
47
|
canEditProfiles: boolean;
|
|
47
48
|
canManageMembers: boolean;
|
|
48
49
|
canManageRoles: boolean;
|
|
50
|
+
canUpdateInvitationRoles: boolean;
|
|
49
51
|
defaultAdminEnabled: boolean;
|
|
50
52
|
isMutating: boolean;
|
|
51
53
|
labels: WorkspaceAccessLabels;
|
|
@@ -57,6 +59,11 @@ type Props = {
|
|
|
57
59
|
userId?: null | string;
|
|
58
60
|
}) => void;
|
|
59
61
|
onRemoveRole: (payload: { roleId: string; userId: string }) => void;
|
|
62
|
+
onUpdateInvitationRole: (payload: {
|
|
63
|
+
email?: null | string;
|
|
64
|
+
roleIds: string[];
|
|
65
|
+
userId?: null | string;
|
|
66
|
+
}) => void;
|
|
60
67
|
roles: Array<Pick<WorkspaceAccessRole, 'id' | 'name'>>;
|
|
61
68
|
};
|
|
62
69
|
|
|
@@ -64,6 +71,7 @@ export function WorkspaceAccessMemberRow({
|
|
|
64
71
|
canEditProfiles,
|
|
65
72
|
canManageMembers,
|
|
66
73
|
canManageRoles,
|
|
74
|
+
canUpdateInvitationRoles,
|
|
67
75
|
defaultAdminEnabled,
|
|
68
76
|
isMutating,
|
|
69
77
|
labels,
|
|
@@ -72,6 +80,7 @@ export function WorkspaceAccessMemberRow({
|
|
|
72
80
|
onEditMemberProfile,
|
|
73
81
|
onRemoveMember,
|
|
74
82
|
onRemoveRole,
|
|
83
|
+
onUpdateInvitationRole,
|
|
75
84
|
roles,
|
|
76
85
|
}: Props) {
|
|
77
86
|
const t = useTranslations();
|
|
@@ -81,6 +90,11 @@ export function WorkspaceAccessMemberRow({
|
|
|
81
90
|
const memberName = getMemberDisplayName(member, t('common.unknown'));
|
|
82
91
|
const guest = member as GuestContext;
|
|
83
92
|
const canRemoveRoles = canManageRoles && memberId && !member.pending;
|
|
93
|
+
const canUpdateInvitationRole =
|
|
94
|
+
canUpdateInvitationRoles &&
|
|
95
|
+
member.pending &&
|
|
96
|
+
member.workspace_member_type !== 'GUEST' &&
|
|
97
|
+
Boolean(memberId || member.email);
|
|
84
98
|
const canEditProfile =
|
|
85
99
|
canEditProfiles &&
|
|
86
100
|
canManageMembers &&
|
|
@@ -281,7 +295,16 @@ export function WorkspaceAccessMemberRow({
|
|
|
281
295
|
</div>
|
|
282
296
|
|
|
283
297
|
<div className="mt-2.5 flex flex-wrap gap-1.5 sm:mt-3">
|
|
284
|
-
{
|
|
298
|
+
{canUpdateInvitationRole ? (
|
|
299
|
+
<WorkspaceAccessInvitationRoleMenu
|
|
300
|
+
email={member.email}
|
|
301
|
+
isMutating={isMutating}
|
|
302
|
+
onUpdate={onUpdateInvitationRole}
|
|
303
|
+
assignedRoles={member.roles}
|
|
304
|
+
roles={roles}
|
|
305
|
+
userId={member.id}
|
|
306
|
+
/>
|
|
307
|
+
) : member.roles.length > 0 ? (
|
|
285
308
|
member.roles.map((role) => (
|
|
286
309
|
<Badge
|
|
287
310
|
key={`${member.id ?? member.email}-${role.id}`}
|
|
@@ -297,7 +320,9 @@ export function WorkspaceAccessMemberRow({
|
|
|
297
320
|
</Badge>
|
|
298
321
|
) : (
|
|
299
322
|
<Badge variant="outline" className="h-5 px-1.5 text-xs">
|
|
300
|
-
{
|
|
323
|
+
{member.pending
|
|
324
|
+
? t('ws-members.no_role_assigned')
|
|
325
|
+
: labels.noRolesLabel}
|
|
301
326
|
</Badge>
|
|
302
327
|
)}
|
|
303
328
|
</div>
|
|
@@ -11,6 +11,7 @@ type Props = {
|
|
|
11
11
|
canEditProfiles: boolean;
|
|
12
12
|
canManageMembers: boolean;
|
|
13
13
|
canManageRoles: boolean;
|
|
14
|
+
canUpdateInvitationRoles: boolean;
|
|
14
15
|
defaultAdminEnabled: boolean;
|
|
15
16
|
isLoading: boolean;
|
|
16
17
|
isMutating: boolean;
|
|
@@ -23,6 +24,11 @@ type Props = {
|
|
|
23
24
|
userId?: null | string;
|
|
24
25
|
}) => void;
|
|
25
26
|
onRemoveRole: (payload: { roleId: string; userId: string }) => void;
|
|
27
|
+
onUpdateInvitationRole: (payload: {
|
|
28
|
+
email?: null | string;
|
|
29
|
+
roleIds: string[];
|
|
30
|
+
userId?: null | string;
|
|
31
|
+
}) => void;
|
|
26
32
|
roles: Array<Pick<WorkspaceAccessRole, 'id' | 'name'>>;
|
|
27
33
|
searchTerm: string;
|
|
28
34
|
status: string;
|
|
@@ -32,6 +38,7 @@ export function WorkspaceAccessMembers({
|
|
|
32
38
|
canEditProfiles,
|
|
33
39
|
canManageMembers,
|
|
34
40
|
canManageRoles,
|
|
41
|
+
canUpdateInvitationRoles,
|
|
35
42
|
defaultAdminEnabled,
|
|
36
43
|
isLoading,
|
|
37
44
|
isMutating,
|
|
@@ -41,6 +48,7 @@ export function WorkspaceAccessMembers({
|
|
|
41
48
|
onEditMemberProfile,
|
|
42
49
|
onRemoveMember,
|
|
43
50
|
onRemoveRole,
|
|
51
|
+
onUpdateInvitationRole,
|
|
44
52
|
roles,
|
|
45
53
|
searchTerm,
|
|
46
54
|
status,
|
|
@@ -83,6 +91,7 @@ export function WorkspaceAccessMembers({
|
|
|
83
91
|
canEditProfiles={canEditProfiles}
|
|
84
92
|
canManageMembers={canManageMembers}
|
|
85
93
|
canManageRoles={canManageRoles}
|
|
94
|
+
canUpdateInvitationRoles={canUpdateInvitationRoles}
|
|
86
95
|
defaultAdminEnabled={defaultAdminEnabled}
|
|
87
96
|
isMutating={isMutating}
|
|
88
97
|
labels={labels}
|
|
@@ -91,6 +100,7 @@ export function WorkspaceAccessMembers({
|
|
|
91
100
|
onEditMemberProfile={onEditMemberProfile}
|
|
92
101
|
onRemoveMember={onRemoveMember}
|
|
93
102
|
onRemoveRole={onRemoveRole}
|
|
103
|
+
onUpdateInvitationRole={onUpdateInvitationRole}
|
|
94
104
|
roles={roles}
|
|
95
105
|
/>
|
|
96
106
|
))}
|