@tuturuuu/ui 0.19.0 → 0.20.1
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 +46 -0
- package/biome.json +1 -1
- package/package.json +62 -50
- package/src/components/ui/badge.tsx +2 -0
- package/src/components/ui/button.tsx +2 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +48 -4
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +22 -0
- package/src/components/ui/custom/animated-slot-text.tsx +20 -0
- package/src/components/ui/custom/common-footer.tsx +262 -237
- package/src/components/ui/custom/language-dropdown-item.tsx +3 -10
- package/src/components/ui/custom/language-toggle.test.tsx +30 -0
- package/src/components/ui/custom/language-toggle.tsx +11 -10
- package/src/components/ui/custom/locale-preference.ts +32 -0
- package/src/components/ui/custom/settings/appearance-settings.tsx +5 -13
- package/src/components/ui/custom/settings-dialog-shell.tsx +40 -9
- package/src/components/ui/custom/structure.tsx +12 -0
- package/src/components/ui/custom/system-language-dropdown-item.tsx +3 -6
- package/src/components/ui/custom/workspace-access/adapters.test.ts +10 -1
- package/src/components/ui/custom/workspace-access/adapters.ts +36 -4
- package/src/components/ui/custom/workspace-access/member-filter-utils.test.ts +14 -0
- package/src/components/ui/custom/workspace-access/member-filter-utils.ts +8 -0
- package/src/components/ui/custom/workspace-access/types.ts +20 -0
- package/src/components/ui/custom/workspace-access/workspace-access-context.test.tsx +111 -0
- package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +15 -5
- package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.tsx +155 -48
- package/src/components/ui/custom/workspace-access/workspace-access-member-profile-dialog.tsx +92 -0
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +171 -76
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +11 -2
- package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +11 -11
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +180 -22
- package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +29 -15
- package/src/components/ui/custom/workspace-access/workspace-access-permission-checklist.tsx +56 -10
- package/src/components/ui/custom/workspace-access/workspace-access-permission-preview.test.ts +33 -0
- package/src/components/ui/custom/workspace-access/workspace-access-permission-preview.tsx +24 -1
- package/src/components/ui/custom/workspace-access/workspace-access-responsive.test.ts +64 -0
- package/src/components/ui/custom/workspace-access/workspace-access-role-editor-dialog.tsx +28 -16
- package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +35 -11
- package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +23 -11
- package/src/components/ui/custom/workspace-select-helpers.ts +5 -3
- package/src/components/ui/custom/workspace-select.tsx +8 -2
- package/src/components/ui/finance/shared/charts/monthly-total-chart-client.tsx +1 -1
- package/src/components/ui/finance/shared/charts/monthly-total-chart.tsx +1 -1
- package/src/components/ui/storefront/cart-summary.tsx +12 -2
- package/src/components/ui/storefront/storefront-surface.test.tsx +21 -0
- package/src/components/ui/storefront/storefront-surface.tsx +6 -0
- package/src/components/ui/text-editor/__tests__/collaboration-binding.test.tsx +161 -0
- package/src/components/ui/text-editor/editor.tsx +267 -235
- package/src/globals.css +166 -0
- package/src/hooks/__tests__/use-workspace-identity-mutation.test.tsx +181 -0
- package/src/hooks/use-workspace-identity-mutation.ts +136 -0
- package/vendor/xlsx/LICENSE +201 -0
- package/vendor/xlsx/types/index.d.ts +1065 -0
- package/vendor/xlsx/xlsx.js +28103 -0
- package/vendor/xlsx/xlsx.mjs +28228 -0
- package/vendor/xlsx-0.20.3.tgz +0 -0
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
4
|
import { TriangleAlert } from '@tuturuuu/icons';
|
|
5
5
|
import type { SupabaseUser } from '@tuturuuu/supabase/next/user';
|
|
6
|
-
import type {
|
|
6
|
+
import type { InternalApiEnhancedWorkspaceMember } from '@tuturuuu/types';
|
|
7
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
7
8
|
import { Skeleton } from '@tuturuuu/ui/skeleton';
|
|
8
9
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
9
10
|
import { Tabs, TabsContent } from '@tuturuuu/ui/tabs';
|
|
@@ -31,6 +32,7 @@ import type {
|
|
|
31
32
|
import { WorkspaceAccessDefaultRoleCard } from './workspace-access-default-role-card';
|
|
32
33
|
import { WorkspaceAccessInviteDialog } from './workspace-access-invite-dialog';
|
|
33
34
|
import { getWorkspaceAccessLabels } from './workspace-access-labels';
|
|
35
|
+
import { WorkspaceAccessMemberProfileDialog } from './workspace-access-member-profile-dialog';
|
|
34
36
|
import { WorkspaceAccessMembers } from './workspace-access-members';
|
|
35
37
|
import { WorkspaceAccessPageHeader } from './workspace-access-page-header';
|
|
36
38
|
import { WorkspaceAccessPeopleFilters } from './workspace-access-people-filters';
|
|
@@ -44,6 +46,7 @@ export function WorkspaceAccessPage({
|
|
|
44
46
|
initialContext,
|
|
45
47
|
initialTab = 'people',
|
|
46
48
|
mode = 'workspace',
|
|
49
|
+
showHeader = true,
|
|
47
50
|
}: WorkspaceAccessPageProps) {
|
|
48
51
|
const t = useTranslations() as (key: string) => string;
|
|
49
52
|
const queryClient = useQueryClient();
|
|
@@ -51,8 +54,11 @@ export function WorkspaceAccessPage({
|
|
|
51
54
|
const [status, setStatus] = useState<WorkspaceAccessMemberStatus>('all');
|
|
52
55
|
const [search, setSearch] = useState('');
|
|
53
56
|
const [inviteEmails, setInviteEmails] = useState('');
|
|
54
|
-
const [
|
|
55
|
-
|
|
57
|
+
const [inviteAccessPreset, setInviteAccessPreset] = useState<
|
|
58
|
+
'guest' | 'member' | 'pos_operator'
|
|
59
|
+
>('member');
|
|
60
|
+
const [confirmDefaultAdminMigration, setConfirmDefaultAdminMigration] =
|
|
61
|
+
useState(false);
|
|
56
62
|
const [inviteDialogOpen, setInviteDialogOpen] = useState(false);
|
|
57
63
|
const [filters, setFilters] = useState<MemberFiltersState>({
|
|
58
64
|
permissionIds: [],
|
|
@@ -60,9 +66,12 @@ export function WorkspaceAccessPage({
|
|
|
60
66
|
});
|
|
61
67
|
const [roleEditorState, setRoleEditorState] =
|
|
62
68
|
useState<WorkspaceAccessRoleEditorState | null>(null);
|
|
69
|
+
const [profileMember, setProfileMember] =
|
|
70
|
+
useState<InternalApiEnhancedWorkspaceMember | null>(null);
|
|
63
71
|
|
|
72
|
+
const fetchesOwnContext = Boolean(adapter.getContext);
|
|
64
73
|
const contextQuery = useQuery({
|
|
65
|
-
enabled:
|
|
74
|
+
enabled: fetchesOwnContext,
|
|
66
75
|
initialData: initialContext,
|
|
67
76
|
queryFn: async () =>
|
|
68
77
|
adapter.getContext
|
|
@@ -71,7 +80,15 @@ export function WorkspaceAccessPage({
|
|
|
71
80
|
queryKey: ['workspace-access', initialContext.workspaceId, 'context'],
|
|
72
81
|
staleTime: 30_000,
|
|
73
82
|
});
|
|
74
|
-
|
|
83
|
+
// Only trust the query when the adapter actually fetches a context. Otherwise
|
|
84
|
+
// `initialData` seeds a *disabled* query, which then serves that first snapshot
|
|
85
|
+
// forever — and callers resolve their permissions asynchronously, so the first
|
|
86
|
+
// snapshot is the one taken before they arrived. That is what left admins
|
|
87
|
+
// looking at a permanently disabled Roles tab: the permission turned true a
|
|
88
|
+
// moment later and this component never saw it.
|
|
89
|
+
const context = fetchesOwnContext
|
|
90
|
+
? (contextQuery.data ?? initialContext)
|
|
91
|
+
: initialContext;
|
|
75
92
|
const workspaceId = context.workspaceId;
|
|
76
93
|
const canManageMembers = context.canManageMembers;
|
|
77
94
|
const canManageRoles = context.canManageRoles;
|
|
@@ -158,13 +175,17 @@ export function WorkspaceAccessPage({
|
|
|
158
175
|
const inviteMutation = useMutation({
|
|
159
176
|
mutationFn: () =>
|
|
160
177
|
adapter.inviteMembers(workspaceId, {
|
|
178
|
+
accessPreset: inviteAccessPreset,
|
|
179
|
+
confirmDefaultAdminMigration,
|
|
161
180
|
emails: parseInviteEmails(inviteEmails),
|
|
162
|
-
memberType:
|
|
181
|
+
memberType: inviteAccessPreset === 'guest' ? 'GUEST' : 'MEMBER',
|
|
163
182
|
}),
|
|
164
183
|
onError: (error) =>
|
|
165
184
|
toast.error(error instanceof Error ? error.message : t('common.error')),
|
|
166
185
|
onSuccess: async (result) => {
|
|
167
186
|
setInviteEmails('');
|
|
187
|
+
setInviteAccessPreset('member');
|
|
188
|
+
setConfirmDefaultAdminMigration(false);
|
|
168
189
|
setInviteDialogOpen(false);
|
|
169
190
|
toast.success(result.message ?? t('ws-members.invitation-sent'));
|
|
170
191
|
await invalidateAccessData();
|
|
@@ -180,6 +201,36 @@ export function WorkspaceAccessPage({
|
|
|
180
201
|
await invalidateAccessData();
|
|
181
202
|
},
|
|
182
203
|
});
|
|
204
|
+
const updateMemberProfileMutation = useMutation({
|
|
205
|
+
mutationFn: ({
|
|
206
|
+
displayName,
|
|
207
|
+
member,
|
|
208
|
+
}: {
|
|
209
|
+
displayName: null | string;
|
|
210
|
+
member: InternalApiEnhancedWorkspaceMember;
|
|
211
|
+
}) => {
|
|
212
|
+
if (!adapter.updateMemberProfile) {
|
|
213
|
+
throw new Error(t('common.error'));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return adapter.updateMemberProfile(workspaceId, {
|
|
217
|
+
displayName,
|
|
218
|
+
email: member.id ? null : member.email,
|
|
219
|
+
userId: member.id,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
onError: (error) =>
|
|
223
|
+
toast.error(
|
|
224
|
+
error instanceof Error
|
|
225
|
+
? error.message
|
|
226
|
+
: t('ws-members.profile_display_name_update_error')
|
|
227
|
+
),
|
|
228
|
+
onSuccess: async () => {
|
|
229
|
+
setProfileMember(null);
|
|
230
|
+
toast.success(t('ws-members.profile_display_name_updated'));
|
|
231
|
+
await invalidateAccessData();
|
|
232
|
+
},
|
|
233
|
+
});
|
|
183
234
|
const roleMembershipMutation = useMutation({
|
|
184
235
|
mutationFn: (payload: {
|
|
185
236
|
action: 'add' | 'remove';
|
|
@@ -205,6 +256,46 @@ export function WorkspaceAccessPage({
|
|
|
205
256
|
await invalidateAccessData();
|
|
206
257
|
},
|
|
207
258
|
});
|
|
259
|
+
const hardenDefaultAdminMutation = useMutation({
|
|
260
|
+
mutationFn: async () => {
|
|
261
|
+
if (!adapter.hardenDefaultAdmin) {
|
|
262
|
+
throw new Error(t('common.error'));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const existingAdminRole = (rolesQuery.data?.data ?? []).find((role) =>
|
|
266
|
+
role.permissions.some(
|
|
267
|
+
(permission) => permission.id === 'admin' && permission.enabled
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
const alreadyAssigned = new Set(
|
|
271
|
+
existingAdminRole?.members?.map((member) => member.id) ?? []
|
|
272
|
+
);
|
|
273
|
+
const memberIds = (membersQuery.data ?? [])
|
|
274
|
+
.filter(
|
|
275
|
+
(member) =>
|
|
276
|
+
!member.pending &&
|
|
277
|
+
member.id &&
|
|
278
|
+
member.workspace_member_type !== 'GUEST' &&
|
|
279
|
+
!alreadyAssigned.has(member.id)
|
|
280
|
+
)
|
|
281
|
+
.map((member) => member.id as string);
|
|
282
|
+
|
|
283
|
+
return adapter.hardenDefaultAdmin(workspaceId, {
|
|
284
|
+
memberIds,
|
|
285
|
+
permissions: [{ enabled: true, id: 'admin' }],
|
|
286
|
+
roleId: existingAdminRole?.id,
|
|
287
|
+
roleName: t('ws-roles.admin'),
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
onError: async (error) => {
|
|
291
|
+
toast.error(error instanceof Error ? error.message : t('common.error'));
|
|
292
|
+
await invalidateAccessData();
|
|
293
|
+
},
|
|
294
|
+
onSuccess: async () => {
|
|
295
|
+
toast.success(t('common.saved'));
|
|
296
|
+
await invalidateAccessData();
|
|
297
|
+
},
|
|
298
|
+
});
|
|
208
299
|
|
|
209
300
|
const roles = rolesQuery.data?.data ?? [];
|
|
210
301
|
const sortedMembers = sortMembers(membersQuery.data ?? []);
|
|
@@ -215,6 +306,11 @@ export function WorkspaceAccessPage({
|
|
|
215
306
|
});
|
|
216
307
|
const joinedCount = sortedMembers.filter((member) => !member.pending).length;
|
|
217
308
|
const invitedCount = sortedMembers.filter((member) => member.pending).length;
|
|
309
|
+
const defaultAdminEnabled = Boolean(
|
|
310
|
+
memberDefaultQuery.data?.permissions.some(
|
|
311
|
+
(permission) => permission.id === 'admin' && permission.enabled
|
|
312
|
+
)
|
|
313
|
+
);
|
|
218
314
|
const filterOptions = getMemberFilterOptions(
|
|
219
315
|
sortedMembers,
|
|
220
316
|
permissionDefinitions
|
|
@@ -222,7 +318,7 @@ export function WorkspaceAccessPage({
|
|
|
222
318
|
|
|
223
319
|
if (contextQuery.isPending) {
|
|
224
320
|
return (
|
|
225
|
-
<div className="space-y-6">
|
|
321
|
+
<div className="space-y-4 sm:space-y-6">
|
|
226
322
|
<Skeleton className="h-32 rounded-xl" />
|
|
227
323
|
<Skeleton className="h-10 rounded-xl" />
|
|
228
324
|
<Skeleton className="h-96 rounded-xl" />
|
|
@@ -231,14 +327,43 @@ export function WorkspaceAccessPage({
|
|
|
231
327
|
}
|
|
232
328
|
|
|
233
329
|
return (
|
|
234
|
-
<div className="space-y-6">
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
330
|
+
<div className="space-y-4 sm:space-y-6">
|
|
331
|
+
{showHeader ? (
|
|
332
|
+
<WorkspaceAccessPageHeader
|
|
333
|
+
context={context}
|
|
334
|
+
invitedCount={invitedCount}
|
|
335
|
+
joinedCount={joinedCount}
|
|
336
|
+
mode={mode}
|
|
337
|
+
totalCount={sortedMembers.length}
|
|
338
|
+
/>
|
|
339
|
+
) : null}
|
|
340
|
+
|
|
341
|
+
{mode === 'workspace' &&
|
|
342
|
+
canManageRoles &&
|
|
343
|
+
defaultAdminEnabled &&
|
|
344
|
+
adapter.hardenDefaultAdmin ? (
|
|
345
|
+
<div className="flex flex-col gap-3 rounded-xl border border-dynamic-orange/25 bg-dynamic-orange/5 p-4 sm:flex-row sm:items-center sm:justify-between">
|
|
346
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
347
|
+
<TriangleAlert className="mt-0.5 size-4 shrink-0 text-dynamic-orange" />
|
|
348
|
+
<div>
|
|
349
|
+
<div className="font-medium text-sm">
|
|
350
|
+
{t('ws-roles.admin_enabled_title')}
|
|
351
|
+
</div>
|
|
352
|
+
<p className="mt-1 text-muted-foreground text-sm">
|
|
353
|
+
{t('ws-members.pos_operator_admin_migration_note')}
|
|
354
|
+
</p>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
<Button
|
|
358
|
+
size="sm"
|
|
359
|
+
className="shrink-0"
|
|
360
|
+
disabled={hardenDefaultAdminMutation.isPending}
|
|
361
|
+
onClick={() => hardenDefaultAdminMutation.mutate()}
|
|
362
|
+
>
|
|
363
|
+
{t('ws-roles.create')}: {t('ws-roles.admin')}
|
|
364
|
+
</Button>
|
|
365
|
+
</div>
|
|
366
|
+
) : null}
|
|
242
367
|
|
|
243
368
|
<Tabs
|
|
244
369
|
value={activeTab}
|
|
@@ -255,7 +380,7 @@ export function WorkspaceAccessPage({
|
|
|
255
380
|
search={search}
|
|
256
381
|
/>
|
|
257
382
|
|
|
258
|
-
<TabsContent value="people" className="mt-
|
|
383
|
+
<TabsContent value="people" className="mt-4 space-y-3 sm:mt-6">
|
|
259
384
|
<WorkspaceAccessPeopleFilters
|
|
260
385
|
filterOptions={filterOptions}
|
|
261
386
|
filters={filters}
|
|
@@ -266,17 +391,22 @@ export function WorkspaceAccessPage({
|
|
|
266
391
|
/>
|
|
267
392
|
|
|
268
393
|
<WorkspaceAccessMembers
|
|
394
|
+
canEditProfiles={Boolean(adapter.updateMemberProfile)}
|
|
269
395
|
canManageMembers={canManageMembers}
|
|
270
396
|
canManageRoles={canManageRoles}
|
|
397
|
+
defaultAdminEnabled={defaultAdminEnabled}
|
|
271
398
|
isLoading={membersQuery.isPending}
|
|
272
399
|
isMutating={
|
|
273
|
-
removeMemberMutation.isPending ||
|
|
400
|
+
removeMemberMutation.isPending ||
|
|
401
|
+
roleMembershipMutation.isPending ||
|
|
402
|
+
updateMemberProfileMutation.isPending
|
|
274
403
|
}
|
|
275
404
|
labels={labels}
|
|
276
405
|
members={visibleMembers}
|
|
277
406
|
onAssignRole={(payload) =>
|
|
278
407
|
roleMembershipMutation.mutate({ ...payload, action: 'add' })
|
|
279
408
|
}
|
|
409
|
+
onEditMemberProfile={setProfileMember}
|
|
280
410
|
onRemoveMember={(payload) => removeMemberMutation.mutate(payload)}
|
|
281
411
|
onRemoveRole={(payload) =>
|
|
282
412
|
roleMembershipMutation.mutate({ ...payload, action: 'remove' })
|
|
@@ -287,7 +417,7 @@ export function WorkspaceAccessPage({
|
|
|
287
417
|
/>
|
|
288
418
|
</TabsContent>
|
|
289
419
|
|
|
290
|
-
<TabsContent value="roles" className="mt-6">
|
|
420
|
+
<TabsContent value="roles" className="mt-4 sm:mt-6">
|
|
291
421
|
<WorkspaceAccessRoles
|
|
292
422
|
canManageRoles={canManageRoles}
|
|
293
423
|
isLoading={rolesQuery.isPending}
|
|
@@ -306,7 +436,7 @@ export function WorkspaceAccessPage({
|
|
|
306
436
|
/>
|
|
307
437
|
</TabsContent>
|
|
308
438
|
|
|
309
|
-
<TabsContent value="defaults-member" className="mt-6">
|
|
439
|
+
<TabsContent value="defaults-member" className="mt-4 sm:mt-6">
|
|
310
440
|
<WorkspaceAccessDefaultRoleCard
|
|
311
441
|
canManageRoles={canManageRoles}
|
|
312
442
|
isLoading={memberDefaultQuery.isPending}
|
|
@@ -324,7 +454,7 @@ export function WorkspaceAccessPage({
|
|
|
324
454
|
/>
|
|
325
455
|
</TabsContent>
|
|
326
456
|
|
|
327
|
-
<TabsContent value="defaults-guest" className="mt-6">
|
|
457
|
+
<TabsContent value="defaults-guest" className="mt-4 sm:mt-6">
|
|
328
458
|
<WorkspaceAccessDefaultRoleCard
|
|
329
459
|
canManageRoles={canManageRoles}
|
|
330
460
|
isLoading={guestDefaultQuery.isPending}
|
|
@@ -344,16 +474,44 @@ export function WorkspaceAccessPage({
|
|
|
344
474
|
</Tabs>
|
|
345
475
|
|
|
346
476
|
<WorkspaceAccessInviteDialog
|
|
477
|
+
accessPreset={inviteAccessPreset}
|
|
478
|
+
canManageRoles={canManageRoles}
|
|
479
|
+
confirmDefaultAdminMigration={confirmDefaultAdminMigration}
|
|
480
|
+
defaultAdminEnabled={defaultAdminEnabled}
|
|
347
481
|
emails={inviteEmails}
|
|
348
482
|
isSubmitting={inviteMutation.isPending}
|
|
349
|
-
|
|
483
|
+
joinedMemberCount={joinedCount}
|
|
484
|
+
onAccessPresetChange={(value) => {
|
|
485
|
+
setInviteAccessPreset(value);
|
|
486
|
+
if (value !== 'pos_operator') {
|
|
487
|
+
setConfirmDefaultAdminMigration(false);
|
|
488
|
+
}
|
|
489
|
+
}}
|
|
490
|
+
onConfirmDefaultAdminMigrationChange={setConfirmDefaultAdminMigration}
|
|
350
491
|
onEmailsChange={setInviteEmails}
|
|
351
|
-
onMemberTypeChange={setInviteMemberType}
|
|
352
492
|
onOpenChange={setInviteDialogOpen}
|
|
353
493
|
onSubmit={() => inviteMutation.mutate()}
|
|
354
494
|
open={inviteDialogOpen}
|
|
355
495
|
/>
|
|
356
496
|
|
|
497
|
+
{profileMember ? (
|
|
498
|
+
<WorkspaceAccessMemberProfileDialog
|
|
499
|
+
key={`${profileMember.id ?? profileMember.email}-profile`}
|
|
500
|
+
isSubmitting={updateMemberProfileMutation.isPending}
|
|
501
|
+
member={profileMember}
|
|
502
|
+
onOpenChange={(open) => {
|
|
503
|
+
if (!open) setProfileMember(null);
|
|
504
|
+
}}
|
|
505
|
+
onSubmit={(displayName) =>
|
|
506
|
+
updateMemberProfileMutation.mutate({
|
|
507
|
+
displayName,
|
|
508
|
+
member: profileMember,
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
open
|
|
512
|
+
/>
|
|
513
|
+
) : null}
|
|
514
|
+
|
|
357
515
|
{roleEditorState ? (
|
|
358
516
|
<WorkspaceAccessRoleEditorDialog
|
|
359
517
|
key={
|
|
@@ -52,38 +52,47 @@ export function WorkspaceAccessPeopleFilters({
|
|
|
52
52
|
filters.permissionIds.length + filters.roleIds.length;
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
|
-
<div className="
|
|
56
|
-
<div className="
|
|
57
|
-
<div className="
|
|
55
|
+
<div className="grid gap-2 rounded-xl border border-border bg-foreground/[0.02] p-2.5 sm:p-3">
|
|
56
|
+
<div className="grid min-w-0 gap-2 sm:grid-cols-[auto_1fr] sm:items-center">
|
|
57
|
+
<div className="hidden items-center gap-2 text-muted-foreground text-sm sm:flex">
|
|
58
58
|
<ListFilter className="h-4 w-4" />
|
|
59
59
|
<span className="font-medium">{t('ws-members.filters')}</span>
|
|
60
60
|
</div>
|
|
61
61
|
|
|
62
62
|
<Tabs
|
|
63
|
+
className="w-full sm:w-auto"
|
|
63
64
|
value={status}
|
|
64
65
|
onValueChange={(value) =>
|
|
65
66
|
onStatusChange(value as WorkspaceAccessMemberStatus)
|
|
66
67
|
}
|
|
67
68
|
>
|
|
68
|
-
<TabsList className="h-
|
|
69
|
-
<TabsTrigger value="all">
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
<TabsList className="grid h-9 w-full grid-cols-3 p-1 sm:w-auto">
|
|
70
|
+
<TabsTrigger className="h-full min-h-0" value="all">
|
|
71
|
+
{t('ws-members.all')}
|
|
72
|
+
</TabsTrigger>
|
|
73
|
+
<TabsTrigger className="h-full min-h-0" value="joined">
|
|
74
|
+
{t('ws-members.joined')}
|
|
75
|
+
</TabsTrigger>
|
|
76
|
+
<TabsTrigger className="h-full min-h-0" value="invited">
|
|
77
|
+
{t('ws-members.invited')}
|
|
78
|
+
</TabsTrigger>
|
|
72
79
|
</TabsList>
|
|
73
80
|
</Tabs>
|
|
74
81
|
</div>
|
|
75
82
|
|
|
76
|
-
<div className="flex
|
|
83
|
+
<div className="flex min-w-0 items-center justify-end gap-2">
|
|
77
84
|
<DropdownMenu>
|
|
78
85
|
<DropdownMenuTrigger asChild>
|
|
79
86
|
<Button
|
|
80
87
|
variant="outline"
|
|
81
88
|
size="sm"
|
|
82
|
-
className="h-8 gap-1.5 border-dashed bg-background/60"
|
|
89
|
+
className="h-8 min-w-0 flex-1 gap-1.5 border-dashed bg-background/60 px-2 sm:flex-none sm:px-3"
|
|
83
90
|
disabled={filterOptions.roles.length === 0}
|
|
84
91
|
>
|
|
85
92
|
<ShieldCheck className="h-4 w-4 text-dynamic-purple" />
|
|
86
|
-
|
|
93
|
+
<span className="truncate sm:max-w-36">
|
|
94
|
+
{labels.filterByRole}
|
|
95
|
+
</span>
|
|
87
96
|
{filters.roleIds.length > 0 ? (
|
|
88
97
|
<>
|
|
89
98
|
<Separator className="mx-1 h-4" orientation="vertical" />
|
|
@@ -132,11 +141,13 @@ export function WorkspaceAccessPeopleFilters({
|
|
|
132
141
|
<Button
|
|
133
142
|
variant="outline"
|
|
134
143
|
size="sm"
|
|
135
|
-
className="h-8 gap-1.5 border-dashed bg-background/60"
|
|
144
|
+
className="h-8 min-w-0 flex-1 gap-1.5 border-dashed bg-background/60 px-2 sm:flex-none sm:px-3"
|
|
136
145
|
disabled={filterOptions.permissions.length === 0}
|
|
137
146
|
>
|
|
138
147
|
<KeyRound className="h-4 w-4 text-dynamic-blue" />
|
|
139
|
-
|
|
148
|
+
<span className="truncate sm:max-w-44">
|
|
149
|
+
{labels.filterByPermission}
|
|
150
|
+
</span>
|
|
140
151
|
{filters.permissionIds.length > 0 ? (
|
|
141
152
|
<>
|
|
142
153
|
<Separator className="mx-1 h-4" orientation="vertical" />
|
|
@@ -181,16 +192,19 @@ export function WorkspaceAccessPeopleFilters({
|
|
|
181
192
|
<Button
|
|
182
193
|
variant="ghost"
|
|
183
194
|
size="sm"
|
|
184
|
-
className="
|
|
195
|
+
className="size-8 shrink-0 px-0 text-muted-foreground hover:text-foreground"
|
|
185
196
|
onClick={() => onFiltersChange({ permissionIds: [], roleIds: [] })}
|
|
197
|
+
aria-label={labels.clearFiltersAction}
|
|
186
198
|
>
|
|
187
199
|
<X className="h-3.5 w-3.5" />
|
|
188
|
-
{labels.clearFiltersAction}
|
|
189
200
|
</Button>
|
|
190
201
|
) : null}
|
|
191
202
|
|
|
192
203
|
{activeFilterCount > 0 ? (
|
|
193
|
-
<Badge
|
|
204
|
+
<Badge
|
|
205
|
+
variant="outline"
|
|
206
|
+
className="hidden rounded-full sm:inline-flex"
|
|
207
|
+
>
|
|
194
208
|
{activeFilterCount}
|
|
195
209
|
</Badge>
|
|
196
210
|
) : null}
|
|
@@ -3,6 +3,7 @@ import { Checkbox } from '@tuturuuu/ui/checkbox';
|
|
|
3
3
|
import { ScrollArea } from '@tuturuuu/ui/scroll-area';
|
|
4
4
|
import { cn } from '@tuturuuu/utils/format';
|
|
5
5
|
import type { permissionGroups } from '@tuturuuu/utils/permissions';
|
|
6
|
+
import { useTranslations } from 'next-intl';
|
|
6
7
|
|
|
7
8
|
type PermissionGroups = ReturnType<typeof permissionGroups>;
|
|
8
9
|
|
|
@@ -17,11 +18,55 @@ export function WorkspaceAccessPermissionChecklist({
|
|
|
17
18
|
onSelectedPermissionsChange,
|
|
18
19
|
selectedPermissions,
|
|
19
20
|
}: Props) {
|
|
21
|
+
const t = useTranslations() as (key: string) => string;
|
|
22
|
+
const adminPermission = groups
|
|
23
|
+
.flatMap((group) => group.permissions)
|
|
24
|
+
.find((permission) => permission.id === 'admin');
|
|
25
|
+
const regularGroups = groups
|
|
26
|
+
.map((group) => ({
|
|
27
|
+
...group,
|
|
28
|
+
permissions: group.permissions.filter(
|
|
29
|
+
(permission) => permission.id !== 'admin'
|
|
30
|
+
),
|
|
31
|
+
}))
|
|
32
|
+
.filter((group) => group.permissions.length > 0);
|
|
33
|
+
const adminEnabled = selectedPermissions.has('admin');
|
|
34
|
+
|
|
20
35
|
return (
|
|
21
|
-
<ScrollArea className="h-
|
|
22
|
-
<div className="space-y-4 p-4">
|
|
23
|
-
{
|
|
24
|
-
<section
|
|
36
|
+
<ScrollArea className="h-auto rounded-lg border sm:h-[52vh]">
|
|
37
|
+
<div className="space-y-3 p-3 sm:space-y-4 sm:p-4">
|
|
38
|
+
{adminPermission ? (
|
|
39
|
+
<section className="rounded-lg border border-dynamic-green/30 bg-dynamic-green/5 p-3 sm:p-4">
|
|
40
|
+
<label className="flex items-start gap-3">
|
|
41
|
+
<Checkbox
|
|
42
|
+
checked={adminEnabled}
|
|
43
|
+
onCheckedChange={(checked) => {
|
|
44
|
+
const next = new Set(selectedPermissions);
|
|
45
|
+
if (checked) next.add('admin');
|
|
46
|
+
else next.delete('admin');
|
|
47
|
+
onSelectedPermissionsChange(next);
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
<div className="min-w-0 flex-1">
|
|
51
|
+
<div className="flex items-center gap-2 font-semibold">
|
|
52
|
+
<ShieldCheck className="size-4 text-dynamic-green" />
|
|
53
|
+
{adminPermission.title}
|
|
54
|
+
</div>
|
|
55
|
+
<p className="mt-1 text-muted-foreground text-sm">
|
|
56
|
+
{adminPermission.description}
|
|
57
|
+
</p>
|
|
58
|
+
{adminEnabled ? (
|
|
59
|
+
<p className="mt-2 rounded-md bg-dynamic-green/10 px-2.5 py-2 text-dynamic-green text-sm">
|
|
60
|
+
{t('ws-roles.admin_enabled_description')}
|
|
61
|
+
</p>
|
|
62
|
+
) : null}
|
|
63
|
+
</div>
|
|
64
|
+
</label>
|
|
65
|
+
</section>
|
|
66
|
+
) : null}
|
|
67
|
+
|
|
68
|
+
{regularGroups.map((group) => (
|
|
69
|
+
<section key={group.id} className="rounded-lg border p-3 sm:p-4">
|
|
25
70
|
<div className="flex items-start gap-3">
|
|
26
71
|
<span className="text-muted-foreground">{group.icon}</span>
|
|
27
72
|
<div>
|
|
@@ -33,17 +78,16 @@ export function WorkspaceAccessPermissionChecklist({
|
|
|
33
78
|
) : null}
|
|
34
79
|
</div>
|
|
35
80
|
</div>
|
|
36
|
-
<div className="mt-
|
|
81
|
+
<div className="mt-3 grid gap-2 sm:mt-4">
|
|
37
82
|
{group.permissions.map((permission) => {
|
|
38
83
|
const checked = selectedPermissions.has(permission.id);
|
|
39
|
-
const adminLocked =
|
|
40
|
-
selectedPermissions.has('admin') && permission.id !== 'admin';
|
|
84
|
+
const adminLocked = adminEnabled;
|
|
41
85
|
|
|
42
86
|
return (
|
|
43
87
|
<label
|
|
44
88
|
key={permission.id}
|
|
45
89
|
className={cn(
|
|
46
|
-
'flex items-start gap-3 rounded-lg border p-3',
|
|
90
|
+
'flex items-start gap-3 rounded-lg border p-2.5 sm:p-3',
|
|
47
91
|
checked || adminLocked
|
|
48
92
|
? 'border-dynamic-green/30 bg-dynamic-green/10'
|
|
49
93
|
: 'bg-background'
|
|
@@ -67,8 +111,10 @@ export function WorkspaceAccessPermissionChecklist({
|
|
|
67
111
|
{permission.icon}
|
|
68
112
|
</span>
|
|
69
113
|
<span>{permission.title}</span>
|
|
70
|
-
{
|
|
71
|
-
<
|
|
114
|
+
{adminLocked ? (
|
|
115
|
+
<span className="ml-auto text-dynamic-green text-xs">
|
|
116
|
+
{t('ws-roles.granted_via_admin')}
|
|
117
|
+
</span>
|
|
72
118
|
) : null}
|
|
73
119
|
</div>
|
|
74
120
|
<p className="mt-1 text-muted-foreground text-sm">
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { enabledPermissionCount } from './workspace-access-permission-preview';
|
|
3
|
+
|
|
4
|
+
describe('enabledPermissionCount', () => {
|
|
5
|
+
it('treats administrator access as granting the full permission catalog', () => {
|
|
6
|
+
expect(
|
|
7
|
+
enabledPermissionCount(
|
|
8
|
+
{
|
|
9
|
+
id: 'admin-role',
|
|
10
|
+
name: 'Administrator',
|
|
11
|
+
permissions: [{ enabled: true, id: 'admin' }],
|
|
12
|
+
},
|
|
13
|
+
42
|
|
14
|
+
)
|
|
15
|
+
).toBe(42);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('counts explicit permissions for non-administrator roles', () => {
|
|
19
|
+
expect(
|
|
20
|
+
enabledPermissionCount(
|
|
21
|
+
{
|
|
22
|
+
id: 'operator-role',
|
|
23
|
+
name: 'Operator',
|
|
24
|
+
permissions: [
|
|
25
|
+
{ enabled: true, id: 'initiate_pos_checkout' },
|
|
26
|
+
{ enabled: false, id: 'update_inventory' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
42
|
|
30
|
+
)
|
|
31
|
+
).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { Badge } from '@tuturuuu/ui/badge';
|
|
2
2
|
import type { WorkspaceAccessRole } from './types';
|
|
3
3
|
|
|
4
|
-
export function enabledPermissionCount(
|
|
4
|
+
export function enabledPermissionCount(
|
|
5
|
+
role?: WorkspaceAccessRole | null,
|
|
6
|
+
permissionCount?: number
|
|
7
|
+
) {
|
|
8
|
+
if (
|
|
9
|
+
permissionCount !== undefined &&
|
|
10
|
+
role?.permissions.some(
|
|
11
|
+
(permission) => permission.id === 'admin' && permission.enabled
|
|
12
|
+
)
|
|
13
|
+
) {
|
|
14
|
+
return permissionCount;
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
return (
|
|
6
18
|
role?.permissions.filter((permission) => permission.enabled).length ?? 0
|
|
7
19
|
);
|
|
@@ -18,6 +30,9 @@ export function WorkspaceAccessPermissionPreview({
|
|
|
18
30
|
}) {
|
|
19
31
|
const enabledPermissions =
|
|
20
32
|
role?.permissions.filter((permission) => permission.enabled) ?? [];
|
|
33
|
+
const adminEnabled = enabledPermissions.some(
|
|
34
|
+
(permission) => permission.id === 'admin'
|
|
35
|
+
);
|
|
21
36
|
|
|
22
37
|
if (enabledPermissions.length === 0) {
|
|
23
38
|
return (
|
|
@@ -27,6 +42,14 @@ export function WorkspaceAccessPermissionPreview({
|
|
|
27
42
|
);
|
|
28
43
|
}
|
|
29
44
|
|
|
45
|
+
if (adminEnabled) {
|
|
46
|
+
return (
|
|
47
|
+
<Badge className="rounded-full border-dynamic-green/30 bg-dynamic-green/10 text-dynamic-green">
|
|
48
|
+
{permissionTitles.get('admin') ?? 'Administrator'}
|
|
49
|
+
</Badge>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
return enabledPermissions.slice(0, 5).map((permission) => (
|
|
31
54
|
<Badge key={permission.id} variant="secondary" className="rounded-full">
|
|
32
55
|
{permissionTitles.get(permission.id) ?? permission.id}
|