@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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChevronDown, Plus, ShieldUser, X } from '@tuturuuu/icons';
|
|
4
|
+
import { Badge } from '@tuturuuu/ui/badge';
|
|
5
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuCheckboxItem,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuLabel,
|
|
12
|
+
DropdownMenuSeparator,
|
|
13
|
+
DropdownMenuTrigger,
|
|
14
|
+
} from '@tuturuuu/ui/dropdown-menu';
|
|
15
|
+
import { useTranslations } from 'next-intl';
|
|
16
|
+
import type { WorkspaceAccessRole } from './types';
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
email?: null | string;
|
|
20
|
+
isMutating: boolean;
|
|
21
|
+
onUpdate: (payload: {
|
|
22
|
+
email?: null | string;
|
|
23
|
+
roleIds: string[];
|
|
24
|
+
userId?: null | string;
|
|
25
|
+
}) => void;
|
|
26
|
+
assignedRoles: Array<Pick<WorkspaceAccessRole, 'id' | 'name'>>;
|
|
27
|
+
roles: Array<Pick<WorkspaceAccessRole, 'id' | 'name'>>;
|
|
28
|
+
userId?: null | string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function WorkspaceAccessInvitationRoleMenu({
|
|
32
|
+
email,
|
|
33
|
+
isMutating,
|
|
34
|
+
onUpdate,
|
|
35
|
+
assignedRoles,
|
|
36
|
+
roles,
|
|
37
|
+
userId,
|
|
38
|
+
}: Props) {
|
|
39
|
+
const t = useTranslations();
|
|
40
|
+
const assignedRoleIds = new Set(assignedRoles.map((role) => role.id));
|
|
41
|
+
const updateRole = (roleId: string) => {
|
|
42
|
+
const roleIds = assignedRoleIds.has(roleId)
|
|
43
|
+
? assignedRoles
|
|
44
|
+
.filter((role) => role.id !== roleId)
|
|
45
|
+
.map((role) => role.id)
|
|
46
|
+
: [...assignedRoles.map((role) => role.id), roleId];
|
|
47
|
+
onUpdate({ email, roleIds, userId });
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="space-y-2">
|
|
52
|
+
{assignedRoles.length > 0 ? (
|
|
53
|
+
<div className="flex flex-wrap gap-1.5">
|
|
54
|
+
{assignedRoles.map((role) => (
|
|
55
|
+
<Badge
|
|
56
|
+
key={role.id}
|
|
57
|
+
className="h-6 gap-1 border-dynamic-purple/35 bg-dynamic-purple/10 px-2 text-dynamic-purple text-xs"
|
|
58
|
+
>
|
|
59
|
+
<ShieldUser className="size-3" />
|
|
60
|
+
{role.name}
|
|
61
|
+
</Badge>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
) : null}
|
|
65
|
+
<DropdownMenu>
|
|
66
|
+
<DropdownMenuTrigger asChild>
|
|
67
|
+
<Button
|
|
68
|
+
variant="outline"
|
|
69
|
+
size="sm"
|
|
70
|
+
className={`h-7 max-w-full rounded-full px-2.5 text-xs active:scale-[0.98] ${assignedRoles.length > 0 ? 'border-dynamic-purple/40 bg-dynamic-purple/10 text-dynamic-purple hover:bg-dynamic-purple/15 hover:text-dynamic-purple' : 'border-dashed text-muted-foreground hover:text-foreground'}`}
|
|
71
|
+
disabled={isMutating}
|
|
72
|
+
>
|
|
73
|
+
{assignedRoles.length > 0 ? (
|
|
74
|
+
<ShieldUser className="size-3.5 shrink-0" />
|
|
75
|
+
) : (
|
|
76
|
+
<Plus className="size-3.5 shrink-0" />
|
|
77
|
+
)}
|
|
78
|
+
<span className="truncate">
|
|
79
|
+
{assignedRoles.length > 0
|
|
80
|
+
? t('ws-members.roles_selected', {
|
|
81
|
+
count: assignedRoles.length,
|
|
82
|
+
})
|
|
83
|
+
: t('ws-members.assign_invitation_role')}
|
|
84
|
+
</span>
|
|
85
|
+
<ChevronDown className="size-3 shrink-0 opacity-60" />
|
|
86
|
+
</Button>
|
|
87
|
+
</DropdownMenuTrigger>
|
|
88
|
+
<DropdownMenuContent align="start" className="w-60">
|
|
89
|
+
<DropdownMenuLabel className="flex items-center gap-2">
|
|
90
|
+
<ShieldUser className="size-4 text-muted-foreground" />
|
|
91
|
+
{t('ws-members.invitation_role')}
|
|
92
|
+
</DropdownMenuLabel>
|
|
93
|
+
<DropdownMenuSeparator />
|
|
94
|
+
{roles.map((option) => (
|
|
95
|
+
<DropdownMenuCheckboxItem
|
|
96
|
+
checked={assignedRoleIds.has(option.id)}
|
|
97
|
+
key={option.id}
|
|
98
|
+
onSelect={(event) => {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
updateRole(option.id);
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<ShieldUser className="size-4" />
|
|
104
|
+
<span className="flex-1 truncate">{option.name}</span>
|
|
105
|
+
</DropdownMenuCheckboxItem>
|
|
106
|
+
))}
|
|
107
|
+
{assignedRoles.length > 0 ? (
|
|
108
|
+
<>
|
|
109
|
+
<DropdownMenuSeparator />
|
|
110
|
+
<DropdownMenuItem
|
|
111
|
+
onSelect={() => onUpdate({ email, roleIds: [], userId })}
|
|
112
|
+
>
|
|
113
|
+
<X className="size-4" />
|
|
114
|
+
{t('ws-members.clear_all_roles')}
|
|
115
|
+
</DropdownMenuItem>
|
|
116
|
+
</>
|
|
117
|
+
) : null}
|
|
118
|
+
</DropdownMenuContent>
|
|
119
|
+
</DropdownMenu>
|
|
120
|
+
<p className="text-muted-foreground text-xs leading-4">
|
|
121
|
+
{t('ws-members.invitation_role_helper')}
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CreditCard, ShieldUser, UserPlus } from '@tuturuuu/icons';
|
|
4
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
|
|
5
|
+
import { useTranslations } from 'next-intl';
|
|
6
|
+
|
|
7
|
+
type AccessPreset = 'guest' | 'member' | 'pos_operator';
|
|
8
|
+
|
|
9
|
+
const OPTIONS: Array<{
|
|
10
|
+
descriptionKey: string;
|
|
11
|
+
icon: typeof UserPlus;
|
|
12
|
+
labelKey: string;
|
|
13
|
+
shortLabelKey: string;
|
|
14
|
+
value: AccessPreset;
|
|
15
|
+
}> = [
|
|
16
|
+
{
|
|
17
|
+
descriptionKey: 'ws-members.invite_membership_member_description',
|
|
18
|
+
icon: UserPlus,
|
|
19
|
+
labelKey: 'ws-members.invite_membership_member',
|
|
20
|
+
shortLabelKey: 'ws-members.invite_access_member_tab',
|
|
21
|
+
value: 'member',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
descriptionKey: 'ws-members.invite_membership_guest_description',
|
|
25
|
+
icon: ShieldUser,
|
|
26
|
+
labelKey: 'ws-members.invite_membership_guest',
|
|
27
|
+
shortLabelKey: 'ws-members.invite_access_guest_tab',
|
|
28
|
+
value: 'guest',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
descriptionKey: 'ws-members.pos_operator_description',
|
|
32
|
+
icon: CreditCard,
|
|
33
|
+
labelKey: 'ws-members.invite_membership_pos_operator',
|
|
34
|
+
shortLabelKey: 'ws-members.invite_access_pos_tab',
|
|
35
|
+
value: 'pos_operator',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export function WorkspaceAccessInviteAccessPicker({
|
|
40
|
+
canManageRoles,
|
|
41
|
+
onChange,
|
|
42
|
+
value,
|
|
43
|
+
}: {
|
|
44
|
+
canManageRoles: boolean;
|
|
45
|
+
onChange: (value: AccessPreset) => void;
|
|
46
|
+
value: AccessPreset;
|
|
47
|
+
}) {
|
|
48
|
+
const t = useTranslations() as (key: string) => string;
|
|
49
|
+
const options = canManageRoles
|
|
50
|
+
? OPTIONS
|
|
51
|
+
: OPTIONS.filter((option) => option.value !== 'pos_operator');
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-2">
|
|
55
|
+
<p className="font-medium text-sm">
|
|
56
|
+
{t('ws-members.invite_membership_label')}
|
|
57
|
+
</p>
|
|
58
|
+
<Tabs
|
|
59
|
+
value={value}
|
|
60
|
+
onValueChange={(nextValue) => onChange(nextValue as AccessPreset)}
|
|
61
|
+
>
|
|
62
|
+
<TabsList
|
|
63
|
+
aria-label={t('ws-members.invite_membership_label')}
|
|
64
|
+
className={`grid h-auto w-full gap-1 rounded-xl bg-muted/60 p-1 ${canManageRoles ? 'grid-cols-3' : 'grid-cols-2'}`}
|
|
65
|
+
>
|
|
66
|
+
{options.map((option) => {
|
|
67
|
+
const Icon = option.icon;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<TabsTrigger
|
|
71
|
+
key={option.value}
|
|
72
|
+
value={option.value}
|
|
73
|
+
className="min-h-10 min-w-0 rounded-lg px-2.5 py-2 data-[state=active]:text-dynamic-blue"
|
|
74
|
+
>
|
|
75
|
+
<Icon className="size-4" />
|
|
76
|
+
<span className="truncate">{t(option.shortLabelKey)}</span>
|
|
77
|
+
</TabsTrigger>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</TabsList>
|
|
81
|
+
|
|
82
|
+
{options.map((option) => {
|
|
83
|
+
const Icon = option.icon;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<TabsContent
|
|
87
|
+
key={option.value}
|
|
88
|
+
value={option.value}
|
|
89
|
+
className="mt-1 rounded-xl border bg-background px-3.5 py-3"
|
|
90
|
+
>
|
|
91
|
+
<div className="flex items-start gap-3">
|
|
92
|
+
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-muted/30 text-dynamic-blue">
|
|
93
|
+
<Icon className="size-4" />
|
|
94
|
+
</span>
|
|
95
|
+
<div className="min-w-0">
|
|
96
|
+
<p className="font-medium text-sm">{t(option.labelKey)}</p>
|
|
97
|
+
<p className="mt-0.5 text-muted-foreground text-xs leading-5">
|
|
98
|
+
{t(option.descriptionKey)}
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</TabsContent>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</Tabs>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { WorkspaceAccessInviteDialog } from './workspace-access-invite-dialog';
|
|
5
|
+
|
|
6
|
+
vi.mock('next-intl', () => ({
|
|
7
|
+
useTranslations: () => (key: string, values?: Record<string, number>) =>
|
|
8
|
+
values?.count === undefined ? key : `${values.count} ${key}`,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe('WorkspaceAccessInviteDialog', () => {
|
|
12
|
+
it('selects multiple workspace roles before the invitation is sent', () => {
|
|
13
|
+
const props = {
|
|
14
|
+
accessPreset: 'member',
|
|
15
|
+
canManageRoles: true,
|
|
16
|
+
confirmDefaultAdminMigration: false,
|
|
17
|
+
defaultAdminEnabled: false,
|
|
18
|
+
emails: 'editor@example.com',
|
|
19
|
+
isSubmitting: false,
|
|
20
|
+
joinedMemberCount: 1,
|
|
21
|
+
noRoleLabel: 'No assigned roles',
|
|
22
|
+
onAccessPresetChange: vi.fn(),
|
|
23
|
+
onConfirmDefaultAdminMigrationChange: vi.fn(),
|
|
24
|
+
onEmailsChange: vi.fn(),
|
|
25
|
+
onOpenChange: vi.fn(),
|
|
26
|
+
onRoleIdsChange: vi.fn(),
|
|
27
|
+
onSubmit: vi.fn(),
|
|
28
|
+
open: true,
|
|
29
|
+
roleIds: [],
|
|
30
|
+
roles: [
|
|
31
|
+
{ id: 'role-editor', name: 'Editor' },
|
|
32
|
+
{ id: 'role-reviewer', name: 'Reviewer' },
|
|
33
|
+
],
|
|
34
|
+
} as ComponentProps<typeof WorkspaceAccessInviteDialog> & {
|
|
35
|
+
noRoleLabel: string;
|
|
36
|
+
onRoleIdsChange: (roleIds: string[]) => void;
|
|
37
|
+
roleIds: string[];
|
|
38
|
+
roles: Array<{ id: string; name: string }>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const { rerender } = render(<WorkspaceAccessInviteDialog {...props} />);
|
|
42
|
+
|
|
43
|
+
expect(
|
|
44
|
+
screen.getByRole('tab', {
|
|
45
|
+
name: 'ws-members.invite_access_member_tab',
|
|
46
|
+
})
|
|
47
|
+
).toHaveAttribute('data-state', 'active');
|
|
48
|
+
expect(screen.getByText('ws-members.role-placeholder')).toBeDefined();
|
|
49
|
+
expect(
|
|
50
|
+
screen.getByPlaceholderText('ws-members.invite_roles_search')
|
|
51
|
+
).toBeDefined();
|
|
52
|
+
|
|
53
|
+
fireEvent.click(screen.getByRole('checkbox', { name: 'Editor' }));
|
|
54
|
+
expect(props.onRoleIdsChange).toHaveBeenLastCalledWith(['role-editor']);
|
|
55
|
+
|
|
56
|
+
rerender(
|
|
57
|
+
<WorkspaceAccessInviteDialog {...props} roleIds={['role-editor']} />
|
|
58
|
+
);
|
|
59
|
+
fireEvent.click(screen.getByRole('checkbox', { name: 'Reviewer' }));
|
|
60
|
+
expect(props.onRoleIdsChange).toHaveBeenLastCalledWith([
|
|
61
|
+
'role-editor',
|
|
62
|
+
'role-reviewer',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
fireEvent.click(screen.getByRole('checkbox', { name: 'Editor' }));
|
|
66
|
+
expect(props.onRoleIdsChange).toHaveBeenLastCalledWith([]);
|
|
67
|
+
|
|
68
|
+
fireEvent.mouseDown(
|
|
69
|
+
screen.getByRole('tab', {
|
|
70
|
+
name: 'ws-members.invite_access_guest_tab',
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
expect(props.onAccessPresetChange).toHaveBeenLastCalledWith('guest');
|
|
74
|
+
|
|
75
|
+
rerender(
|
|
76
|
+
<WorkspaceAccessInviteDialog
|
|
77
|
+
{...props}
|
|
78
|
+
accessPreset="guest"
|
|
79
|
+
roleIds={[]}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
expect(screen.queryByRole('checkbox', { name: 'Editor' })).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('filters roles, clears the selection, and lets the role section collapse', () => {
|
|
86
|
+
const onRoleIdsChange = vi.fn();
|
|
87
|
+
|
|
88
|
+
render(
|
|
89
|
+
<WorkspaceAccessInviteDialog
|
|
90
|
+
accessPreset="member"
|
|
91
|
+
canManageRoles
|
|
92
|
+
confirmDefaultAdminMigration={false}
|
|
93
|
+
defaultAdminEnabled={false}
|
|
94
|
+
emails="editor@example.com"
|
|
95
|
+
isSubmitting={false}
|
|
96
|
+
joinedMemberCount={1}
|
|
97
|
+
noRoleLabel="No assigned roles"
|
|
98
|
+
onAccessPresetChange={vi.fn()}
|
|
99
|
+
onConfirmDefaultAdminMigrationChange={vi.fn()}
|
|
100
|
+
onEmailsChange={vi.fn()}
|
|
101
|
+
onOpenChange={vi.fn()}
|
|
102
|
+
onRoleIdsChange={onRoleIdsChange}
|
|
103
|
+
onSubmit={vi.fn()}
|
|
104
|
+
open
|
|
105
|
+
roleIds={['role-editor', 'role-reviewer']}
|
|
106
|
+
roles={[
|
|
107
|
+
{ id: 'role-editor', name: 'Editor' },
|
|
108
|
+
{ id: 'role-reviewer', name: 'Reviewer' },
|
|
109
|
+
]}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
fireEvent.change(
|
|
114
|
+
screen.getByPlaceholderText('ws-members.invite_roles_search'),
|
|
115
|
+
{ target: { value: 'review' } }
|
|
116
|
+
);
|
|
117
|
+
expect(screen.queryByRole('checkbox', { name: 'Editor' })).toBeNull();
|
|
118
|
+
expect(screen.getByRole('checkbox', { name: 'Reviewer' })).toBeDefined();
|
|
119
|
+
|
|
120
|
+
fireEvent.click(
|
|
121
|
+
screen.getByRole('button', { name: 'ws-members.clear_all_roles' })
|
|
122
|
+
);
|
|
123
|
+
expect(onRoleIdsChange).toHaveBeenLastCalledWith([]);
|
|
124
|
+
|
|
125
|
+
fireEvent.click(
|
|
126
|
+
screen.getByRole('button', {
|
|
127
|
+
name: /ws-members\.role-placeholder/,
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
expect(
|
|
131
|
+
screen.queryByPlaceholderText('ws-members.invite_roles_search')
|
|
132
|
+
).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -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={
|