@tuturuuu/ui 0.7.0 → 0.8.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 +48 -0
- package/package.json +8 -8
- package/src/components/ui/currency-input.test.tsx +43 -0
- package/src/components/ui/currency-input.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
- package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
- package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
- package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
- package/src/components/ui/finance/transactions/form-types.ts +2 -0
- package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
- package/src/components/ui/money-input.test.tsx +64 -0
- package/src/components/ui/money-input.tsx +63 -0
- package/src/components/ui/storefront/cart-summary.tsx +114 -29
- package/src/components/ui/storefront/checkout-overlay.tsx +27 -0
- package/src/components/ui/storefront/hero-panel.tsx +2 -8
- package/src/components/ui/storefront/image-panel.tsx +6 -0
- package/src/components/ui/storefront/index.ts +11 -0
- package/src/components/ui/storefront/listing-card.tsx +84 -22
- package/src/components/ui/storefront/product-detail.tsx +289 -0
- package/src/components/ui/storefront/product-dialog.tsx +72 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +124 -1
- package/src/components/ui/storefront/storefront-surface.tsx +333 -133
- package/src/components/ui/storefront/types.ts +23 -1
- package/src/components/ui/storefront/utils.ts +111 -27
- package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
- package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
- package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
- package/src/components/ui/text-editor/content-migration.ts +41 -18
- package/src/components/ui/text-editor/extensions.ts +1 -1
- package/src/components/ui/text-editor/image-extension.ts +40 -18
- package/src/components/ui/text-editor/video-extension.ts +11 -2
- package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
- package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +1 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +13 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +46 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +13 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +3 -1
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +56 -2
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
- package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +34 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +39 -0
- package/src/components/ui/tu-do/shared/board-views.tsx +9 -7
- package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
- package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +42 -14
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
- package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
- package/src/hooks/useBoardRealtime.ts +6 -3
- package/src/hooks/useBoardRealtime.types.ts +11 -0
- package/src/hooks/useCursorTracking.ts +91 -27
- package/src/hooks/useTaskUserRealtime.ts +5 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
|
-
import {
|
|
4
|
+
import { TriangleAlert } from '@tuturuuu/icons';
|
|
5
5
|
import type { SupabaseUser } from '@tuturuuu/supabase/next/user';
|
|
6
6
|
import type { WorkspaceDefaultPermissionMemberType } from '@tuturuuu/types';
|
|
7
7
|
import { Skeleton } from '@tuturuuu/ui/skeleton';
|
|
@@ -222,10 +222,10 @@ export function WorkspaceAccessPage({
|
|
|
222
222
|
|
|
223
223
|
if (contextQuery.isPending) {
|
|
224
224
|
return (
|
|
225
|
-
<div className="space-y-
|
|
226
|
-
<Skeleton className="h-
|
|
227
|
-
<Skeleton className="h-
|
|
228
|
-
<Skeleton className="h-96 rounded-
|
|
225
|
+
<div className="space-y-6">
|
|
226
|
+
<Skeleton className="h-32 rounded-xl" />
|
|
227
|
+
<Skeleton className="h-10 rounded-xl" />
|
|
228
|
+
<Skeleton className="h-96 rounded-xl" />
|
|
229
229
|
</div>
|
|
230
230
|
);
|
|
231
231
|
}
|
|
@@ -255,42 +255,39 @@ export function WorkspaceAccessPage({
|
|
|
255
255
|
search={search}
|
|
256
256
|
/>
|
|
257
257
|
|
|
258
|
-
<TabsContent value="people" className="mt-4">
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
/>
|
|
258
|
+
<TabsContent value="people" className="mt-6 space-y-4">
|
|
259
|
+
<WorkspaceAccessPeopleFilters
|
|
260
|
+
filterOptions={filterOptions}
|
|
261
|
+
filters={filters}
|
|
262
|
+
labels={labels}
|
|
263
|
+
onFiltersChange={setFilters}
|
|
264
|
+
onStatusChange={setStatus}
|
|
265
|
+
status={status}
|
|
266
|
+
/>
|
|
268
267
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
/>
|
|
290
|
-
</div>
|
|
268
|
+
<WorkspaceAccessMembers
|
|
269
|
+
canManageMembers={canManageMembers}
|
|
270
|
+
canManageRoles={canManageRoles}
|
|
271
|
+
isLoading={membersQuery.isPending}
|
|
272
|
+
isMutating={
|
|
273
|
+
removeMemberMutation.isPending || roleMembershipMutation.isPending
|
|
274
|
+
}
|
|
275
|
+
labels={labels}
|
|
276
|
+
members={visibleMembers}
|
|
277
|
+
onAssignRole={(payload) =>
|
|
278
|
+
roleMembershipMutation.mutate({ ...payload, action: 'add' })
|
|
279
|
+
}
|
|
280
|
+
onRemoveMember={(payload) => removeMemberMutation.mutate(payload)}
|
|
281
|
+
onRemoveRole={(payload) =>
|
|
282
|
+
roleMembershipMutation.mutate({ ...payload, action: 'remove' })
|
|
283
|
+
}
|
|
284
|
+
roles={roles}
|
|
285
|
+
searchTerm={search}
|
|
286
|
+
status={status}
|
|
287
|
+
/>
|
|
291
288
|
</TabsContent>
|
|
292
289
|
|
|
293
|
-
<TabsContent value="roles" className="mt-
|
|
290
|
+
<TabsContent value="roles" className="mt-6">
|
|
294
291
|
<WorkspaceAccessRoles
|
|
295
292
|
canManageRoles={canManageRoles}
|
|
296
293
|
isLoading={rolesQuery.isPending}
|
|
@@ -309,7 +306,7 @@ export function WorkspaceAccessPage({
|
|
|
309
306
|
/>
|
|
310
307
|
</TabsContent>
|
|
311
308
|
|
|
312
|
-
<TabsContent value="defaults-member" className="mt-
|
|
309
|
+
<TabsContent value="defaults-member" className="mt-6">
|
|
313
310
|
<WorkspaceAccessDefaultRoleCard
|
|
314
311
|
canManageRoles={canManageRoles}
|
|
315
312
|
isLoading={memberDefaultQuery.isPending}
|
|
@@ -327,7 +324,7 @@ export function WorkspaceAccessPage({
|
|
|
327
324
|
/>
|
|
328
325
|
</TabsContent>
|
|
329
326
|
|
|
330
|
-
<TabsContent value="defaults-guest" className="mt-
|
|
327
|
+
<TabsContent value="defaults-guest" className="mt-6">
|
|
331
328
|
<WorkspaceAccessDefaultRoleCard
|
|
332
329
|
canManageRoles={canManageRoles}
|
|
333
330
|
isLoading={guestDefaultQuery.isPending}
|
|
@@ -378,8 +375,8 @@ export function WorkspaceAccessPage({
|
|
|
378
375
|
) : null}
|
|
379
376
|
|
|
380
377
|
{contextQuery.isError ? (
|
|
381
|
-
<div className="rounded-
|
|
382
|
-
<
|
|
378
|
+
<div className="flex items-center gap-2 rounded-xl border border-dynamic-red/20 bg-dynamic-red/5 p-4 text-dynamic-red text-sm">
|
|
379
|
+
<TriangleAlert className="h-4 w-4 shrink-0" />
|
|
383
380
|
{t('common.error')}
|
|
384
381
|
</div>
|
|
385
382
|
) : null}
|
|
@@ -52,7 +52,7 @@ export function WorkspaceAccessPeopleFilters({
|
|
|
52
52
|
filters.permissionIds.length + filters.roleIds.length;
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
|
-
<div className="
|
|
55
|
+
<div className="flex flex-col gap-3 rounded-xl border border-border bg-foreground/[0.02] p-3 sm:flex-row sm:items-center sm:justify-between">
|
|
56
56
|
<div className="flex flex-wrap items-center gap-2">
|
|
57
57
|
<div className="mr-1 flex items-center gap-2 text-muted-foreground text-sm">
|
|
58
58
|
<ListFilter className="h-4 w-4" />
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Pencil, Plus, Trash2 } from '@tuturuuu/icons';
|
|
3
|
+
import { Pencil, Plus, ShieldCheck, Trash2, Users } from '@tuturuuu/icons';
|
|
4
4
|
import type { InternalApiEnhancedWorkspaceMember } from '@tuturuuu/types';
|
|
5
5
|
import { Badge } from '@tuturuuu/ui/badge';
|
|
6
6
|
import { Button } from '@tuturuuu/ui/button';
|
|
7
|
-
import {
|
|
8
|
-
Card,
|
|
9
|
-
CardContent,
|
|
10
|
-
CardDescription,
|
|
11
|
-
CardHeader,
|
|
12
|
-
CardTitle,
|
|
13
|
-
} from '@tuturuuu/ui/card';
|
|
14
7
|
import { Skeleton } from '@tuturuuu/ui/skeleton';
|
|
15
8
|
import { useTranslations } from 'next-intl';
|
|
16
9
|
import { getMemberDisplayName } from './member-filter-utils';
|
|
@@ -56,37 +49,36 @@ export function WorkspaceAccessRoles({
|
|
|
56
49
|
|
|
57
50
|
return (
|
|
58
51
|
<div className="space-y-4">
|
|
59
|
-
<
|
|
60
|
-
<
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
</CardHeader>
|
|
74
|
-
</Card>
|
|
52
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
53
|
+
<div>
|
|
54
|
+
<h2 className="font-semibold text-lg">{labels.accessLevelsLabel}</h2>
|
|
55
|
+
<p className="text-muted-foreground text-sm">
|
|
56
|
+
{t('ws-roles.description')}
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
{canManageRoles ? (
|
|
60
|
+
<Button onClick={onCreateRole}>
|
|
61
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
62
|
+
{t('ws-roles.create')}
|
|
63
|
+
</Button>
|
|
64
|
+
) : null}
|
|
65
|
+
</div>
|
|
75
66
|
|
|
76
67
|
{isLoading ? (
|
|
77
68
|
<div className="space-y-3">
|
|
78
|
-
<Skeleton className="h-36 rounded-
|
|
79
|
-
<Skeleton className="h-36 rounded-
|
|
69
|
+
<Skeleton className="h-36 rounded-xl" />
|
|
70
|
+
<Skeleton className="h-36 rounded-xl" />
|
|
80
71
|
</div>
|
|
81
72
|
) : roles.length === 0 ? (
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
73
|
+
<div className="flex min-h-44 flex-col items-center justify-center gap-2 rounded-xl border border-border border-dashed p-8 text-center">
|
|
74
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-dynamic-purple/10 text-dynamic-purple">
|
|
75
|
+
<ShieldCheck className="h-6 w-6" />
|
|
76
|
+
</div>
|
|
77
|
+
<div className="font-medium">{labels.rolesEmptyTitle}</div>
|
|
78
|
+
<div className="max-w-sm text-muted-foreground text-sm">
|
|
79
|
+
{labels.rolesEmptyDescription}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
90
82
|
) : (
|
|
91
83
|
<div className="grid gap-3">
|
|
92
84
|
{roles.map((role) => {
|
|
@@ -94,74 +86,104 @@ export function WorkspaceAccessRoles({
|
|
|
94
86
|
role.members && role.members.length > 0
|
|
95
87
|
? role.members
|
|
96
88
|
: assignedMembersForRole(role.id, members);
|
|
89
|
+
const enabled = enabledPermissionCount(role);
|
|
90
|
+
const pct =
|
|
91
|
+
permissionCount > 0
|
|
92
|
+
? Math.round((enabled / permissionCount) * 100)
|
|
93
|
+
: 0;
|
|
97
94
|
|
|
98
95
|
return (
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
</span>
|
|
108
|
-
<span>
|
|
109
|
-
{t('ws-roles.permissions')}:{' '}
|
|
110
|
-
{enabledPermissionCount(role)}/{permissionCount}
|
|
111
|
-
</span>
|
|
112
|
-
</div>
|
|
96
|
+
<div
|
|
97
|
+
key={role.id}
|
|
98
|
+
className="rounded-xl border border-border bg-background p-5 transition-colors hover:border-foreground/20"
|
|
99
|
+
>
|
|
100
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
101
|
+
<div className="flex min-w-0 gap-3">
|
|
102
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-dynamic-purple/30 bg-dynamic-purple/10 text-dynamic-purple">
|
|
103
|
+
<ShieldCheck className="h-5 w-5" />
|
|
113
104
|
</div>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
>
|
|
121
|
-
<Pencil className="mr-2 h-4 w-4" />
|
|
122
|
-
{t('common.edit')}
|
|
123
|
-
</Button>
|
|
124
|
-
<Button
|
|
105
|
+
<div className="min-w-0">
|
|
106
|
+
<div className="flex items-center gap-2">
|
|
107
|
+
<span className="truncate font-semibold text-base">
|
|
108
|
+
{role.name}
|
|
109
|
+
</span>
|
|
110
|
+
<Badge
|
|
125
111
|
variant="outline"
|
|
126
|
-
|
|
112
|
+
className="h-5 gap-1 px-1.5 text-xs"
|
|
127
113
|
>
|
|
128
|
-
<
|
|
129
|
-
{
|
|
130
|
-
</
|
|
114
|
+
<Users className="h-3 w-3" />
|
|
115
|
+
{assignedMembers.length}
|
|
116
|
+
</Badge>
|
|
131
117
|
</div>
|
|
132
|
-
|
|
118
|
+
<div className="mt-2 max-w-xs">
|
|
119
|
+
<div className="flex items-center justify-between text-muted-foreground text-xs">
|
|
120
|
+
<span>{t('ws-roles.permissions')}</span>
|
|
121
|
+
<span className="tabular-nums">
|
|
122
|
+
{enabled}/{permissionCount}
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
<div className="mt-1 h-1.5 overflow-hidden rounded-full bg-foreground/10">
|
|
126
|
+
<div
|
|
127
|
+
className="h-full rounded-full bg-dynamic-purple"
|
|
128
|
+
style={{ width: `${pct}%` }}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
133
|
</div>
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
{canManageRoles ? (
|
|
136
|
+
<div className="flex flex-wrap gap-2">
|
|
137
|
+
<Button
|
|
138
|
+
variant="outline"
|
|
139
|
+
size="sm"
|
|
140
|
+
onClick={() => onEditRole(role)}
|
|
141
|
+
>
|
|
142
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
143
|
+
{t('common.edit')}
|
|
144
|
+
</Button>
|
|
145
|
+
<Button
|
|
146
|
+
variant="outline"
|
|
147
|
+
size="sm"
|
|
148
|
+
onClick={() => onDeleteRole(role)}
|
|
149
|
+
>
|
|
150
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
151
|
+
{t('common.delete')}
|
|
152
|
+
</Button>
|
|
153
|
+
</div>
|
|
154
|
+
) : null}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
158
|
+
<WorkspaceAccessPermissionPreview
|
|
159
|
+
emptyLabel={t('ws-members.no_permissions')}
|
|
160
|
+
permissionTitles={permissionTitles}
|
|
161
|
+
role={role}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
142
164
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
165
|
+
{assignedMembers.length > 0 ? (
|
|
166
|
+
<div className="mt-3 flex flex-wrap gap-1.5 border-border border-t pt-3">
|
|
167
|
+
{assignedMembers.slice(0, 6).map((member) => (
|
|
168
|
+
<Badge
|
|
169
|
+
key={`${role.id}-${member.id}`}
|
|
170
|
+
variant="secondary"
|
|
171
|
+
className="rounded-full text-xs"
|
|
172
|
+
>
|
|
173
|
+
{getMemberDisplayName(
|
|
174
|
+
member as InternalApiEnhancedWorkspaceMember,
|
|
175
|
+
t('common.unknown')
|
|
176
|
+
)}
|
|
147
177
|
</Badge>
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
>
|
|
155
|
-
{getMemberDisplayName(
|
|
156
|
-
member as InternalApiEnhancedWorkspaceMember,
|
|
157
|
-
t('common.unknown')
|
|
158
|
-
)}
|
|
159
|
-
</Badge>
|
|
160
|
-
))
|
|
161
|
-
)}
|
|
178
|
+
))}
|
|
179
|
+
{assignedMembers.length > 6 ? (
|
|
180
|
+
<Badge variant="outline" className="rounded-full text-xs">
|
|
181
|
+
+{assignedMembers.length - 6}
|
|
182
|
+
</Badge>
|
|
183
|
+
) : null}
|
|
162
184
|
</div>
|
|
163
|
-
|
|
164
|
-
</
|
|
185
|
+
) : null}
|
|
186
|
+
</div>
|
|
165
187
|
);
|
|
166
188
|
})}
|
|
167
189
|
</div>
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
KeyRound,
|
|
5
|
+
Search,
|
|
6
|
+
ShieldCheck,
|
|
7
|
+
ShieldUser,
|
|
8
|
+
UserPlus,
|
|
9
|
+
Users,
|
|
10
|
+
} from '@tuturuuu/icons';
|
|
4
11
|
import { Button } from '@tuturuuu/ui/button';
|
|
5
12
|
import { Input } from '@tuturuuu/ui/input';
|
|
6
13
|
import { TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
|
|
7
14
|
import { useTranslations } from 'next-intl';
|
|
15
|
+
import type { ReactNode } from 'react';
|
|
8
16
|
import type { WorkspaceAccessTab } from './types';
|
|
9
17
|
|
|
10
18
|
type Props = {
|
|
@@ -18,6 +26,9 @@ type Props = {
|
|
|
18
26
|
search: string;
|
|
19
27
|
};
|
|
20
28
|
|
|
29
|
+
const TAB_TRIGGER_CLASS =
|
|
30
|
+
'rounded-none border-transparent border-b-2 bg-transparent px-1 pt-1 pb-3 text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:border-dynamic-blue data-[state=active]:bg-transparent data-[state=active]:text-foreground data-[state=active]:shadow-none';
|
|
31
|
+
|
|
21
32
|
export function WorkspaceAccessTabsToolbar({
|
|
22
33
|
activeTab,
|
|
23
34
|
accessLevelsLabel,
|
|
@@ -30,41 +41,71 @@ export function WorkspaceAccessTabsToolbar({
|
|
|
30
41
|
}: Props) {
|
|
31
42
|
const t = useTranslations() as (key: string) => string;
|
|
32
43
|
|
|
44
|
+
const tabs: Array<{
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
icon: ReactNode;
|
|
47
|
+
label: string;
|
|
48
|
+
value: WorkspaceAccessTab;
|
|
49
|
+
}> = [
|
|
50
|
+
{
|
|
51
|
+
icon: <Users className="h-4 w-4" />,
|
|
52
|
+
label: t('ws-roles.members'),
|
|
53
|
+
value: 'people',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
disabled: !canManageRoles,
|
|
57
|
+
icon: <ShieldCheck className="h-4 w-4" />,
|
|
58
|
+
label: accessLevelsLabel,
|
|
59
|
+
value: 'roles',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
disabled: !canManageRoles,
|
|
63
|
+
icon: <ShieldUser className="h-4 w-4" />,
|
|
64
|
+
label: t('ws-roles.member_defaults_tab'),
|
|
65
|
+
value: 'defaults-member',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
disabled: !canManageRoles,
|
|
69
|
+
icon: <KeyRound className="h-4 w-4" />,
|
|
70
|
+
label: t('ws-roles.guest_defaults_tab'),
|
|
71
|
+
value: 'defaults-guest',
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
33
75
|
return (
|
|
34
|
-
<div className="
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
<TabsTrigger
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{t('ws-roles.guest_defaults_tab')}
|
|
76
|
+
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
|
|
77
|
+
<TabsList className="h-auto w-full justify-start gap-5 overflow-x-auto rounded-none border-border border-b bg-transparent p-0 lg:w-auto">
|
|
78
|
+
{tabs.map((tab) => (
|
|
79
|
+
<TabsTrigger
|
|
80
|
+
key={tab.value}
|
|
81
|
+
value={tab.value}
|
|
82
|
+
disabled={tab.disabled}
|
|
83
|
+
className={TAB_TRIGGER_CLASS}
|
|
84
|
+
>
|
|
85
|
+
{tab.icon}
|
|
86
|
+
{tab.label}
|
|
46
87
|
</TabsTrigger>
|
|
47
|
-
|
|
88
|
+
))}
|
|
89
|
+
</TabsList>
|
|
48
90
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
</div>
|
|
59
|
-
{activeTab === 'people' ? (
|
|
60
|
-
<Button disabled={!canInvite} onClick={onInviteClick}>
|
|
61
|
-
<UserPlus className="mr-2 h-4 w-4" />
|
|
62
|
-
{disableInvite
|
|
63
|
-
? t('ws-members.invite_member_disabled')
|
|
64
|
-
: t('ws-members.invite_member')}
|
|
65
|
-
</Button>
|
|
66
|
-
) : null}
|
|
91
|
+
<div className="flex w-full shrink-0 flex-col gap-2 sm:flex-row lg:w-auto">
|
|
92
|
+
<div className="relative min-w-0 sm:min-w-[280px]">
|
|
93
|
+
<Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
94
|
+
<Input
|
|
95
|
+
value={search}
|
|
96
|
+
onChange={(event) => onSearchChange(event.target.value)}
|
|
97
|
+
placeholder={t('common.search')}
|
|
98
|
+
className="pl-9"
|
|
99
|
+
/>
|
|
67
100
|
</div>
|
|
101
|
+
{activeTab === 'people' ? (
|
|
102
|
+
<Button disabled={!canInvite} onClick={onInviteClick}>
|
|
103
|
+
<UserPlus className="mr-2 h-4 w-4" />
|
|
104
|
+
{disableInvite
|
|
105
|
+
? t('ws-members.invite_member_disabled')
|
|
106
|
+
: t('ws-members.invite_member')}
|
|
107
|
+
</Button>
|
|
108
|
+
) : null}
|
|
68
109
|
</div>
|
|
69
110
|
</div>
|
|
70
111
|
);
|
|
@@ -85,12 +85,6 @@ export function TransactionCard({
|
|
|
85
85
|
useFinanceConfidentialVisibility();
|
|
86
86
|
const isTransfer = !!transaction.transfer;
|
|
87
87
|
|
|
88
|
-
// Check if transaction is confidential
|
|
89
|
-
const isConfidential =
|
|
90
|
-
transaction.is_amount_confidential ||
|
|
91
|
-
transaction.is_description_confidential ||
|
|
92
|
-
transaction.is_category_confidential;
|
|
93
|
-
|
|
94
88
|
// Get custom icon if available
|
|
95
89
|
const CategoryIcon = useMemo(() => {
|
|
96
90
|
if (transaction.category_icon) {
|
|
@@ -127,10 +121,17 @@ export function TransactionCard({
|
|
|
127
121
|
return wallets.find((w) => w.id === transaction.transfer?.linked_wallet_id);
|
|
128
122
|
}, [transaction.transfer?.linked_wallet_id, wallets]);
|
|
129
123
|
|
|
124
|
+
const linkedAmount = transaction.transfer?.linked_amount_redacted
|
|
125
|
+
? undefined
|
|
126
|
+
: transaction.transfer?.linked_amount;
|
|
127
|
+
const linkedAmountIsConfidential = Boolean(
|
|
128
|
+
transaction.transfer?.linked_is_amount_confidential
|
|
129
|
+
);
|
|
130
130
|
const transferDisplay = transaction.transfer
|
|
131
131
|
? transaction.transfer.is_origin
|
|
132
132
|
? {
|
|
133
133
|
amount: transaction.amount,
|
|
134
|
+
amountIsConfidential: Boolean(transaction.is_amount_confidential),
|
|
134
135
|
amountCurrency: transaction.wallet_currency,
|
|
135
136
|
destinationIcon: linkedWallet?.icon,
|
|
136
137
|
destinationImageSrc: linkedWallet?.image_src,
|
|
@@ -140,11 +141,15 @@ export function TransactionCard({
|
|
|
140
141
|
originImageSrc: wallet?.image_src,
|
|
141
142
|
originWalletId: transaction.wallet_id,
|
|
142
143
|
originWalletName: transaction.wallet,
|
|
143
|
-
secondaryAmount:
|
|
144
|
+
secondaryAmount: linkedAmount,
|
|
144
145
|
secondaryCurrency: transaction.transfer.linked_wallet_currency,
|
|
145
146
|
}
|
|
146
147
|
: {
|
|
147
|
-
amount:
|
|
148
|
+
amount: linkedAmount ?? transaction.amount,
|
|
149
|
+
amountIsConfidential:
|
|
150
|
+
linkedAmount != null
|
|
151
|
+
? linkedAmountIsConfidential
|
|
152
|
+
: Boolean(transaction.is_amount_confidential),
|
|
148
153
|
amountCurrency:
|
|
149
154
|
transaction.transfer.linked_wallet_currency ||
|
|
150
155
|
transaction.wallet_currency,
|
|
@@ -161,9 +166,16 @@ export function TransactionCard({
|
|
|
161
166
|
}
|
|
162
167
|
: null;
|
|
163
168
|
const displayAmount = transferDisplay?.amount ?? transaction.amount;
|
|
169
|
+
const displayAmountIsConfidential =
|
|
170
|
+
transferDisplay?.amountIsConfidential ??
|
|
171
|
+
Boolean(transaction.is_amount_confidential);
|
|
164
172
|
const effectiveCurrency =
|
|
165
173
|
transferDisplay?.amountCurrency || transaction.wallet_currency || currency;
|
|
166
174
|
const isExpense = (displayAmount || 0) < 0;
|
|
175
|
+
const isConfidential =
|
|
176
|
+
displayAmountIsConfidential ||
|
|
177
|
+
transaction.is_description_confidential ||
|
|
178
|
+
transaction.is_category_confidential;
|
|
167
179
|
|
|
168
180
|
// Currency conversion for foreign-currency transactions
|
|
169
181
|
const isForeignCurrency =
|
|
@@ -434,7 +446,7 @@ export function TransactionCard({
|
|
|
434
446
|
<div className="flex flex-col items-end">
|
|
435
447
|
<ConfidentialAmount
|
|
436
448
|
amount={displayAmount ?? null}
|
|
437
|
-
isConfidential={
|
|
449
|
+
isConfidential={displayAmountIsConfidential}
|
|
438
450
|
currency={effectiveCurrency}
|
|
439
451
|
className={cn(
|
|
440
452
|
'font-bold text-sm tabular-nums transition-all duration-200 sm:text-xl',
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { MoneyInput } from './money-input';
|
|
4
|
+
|
|
5
|
+
describe('MoneyInput', () => {
|
|
6
|
+
it('renders a minor-unit USD value as major units', () => {
|
|
7
|
+
render(
|
|
8
|
+
<MoneyInput
|
|
9
|
+
aria-label="Price"
|
|
10
|
+
currency="USD"
|
|
11
|
+
hideHelpers
|
|
12
|
+
onChange={vi.fn()}
|
|
13
|
+
value={10000}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const input = screen.getByLabelText('Price') as HTMLInputElement;
|
|
18
|
+
expect(input).toHaveValue('100');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('emits minor units (cents) for a USD entry', () => {
|
|
22
|
+
const onChange = vi.fn();
|
|
23
|
+
render(
|
|
24
|
+
<MoneyInput
|
|
25
|
+
aria-label="Price"
|
|
26
|
+
currency="USD"
|
|
27
|
+
hideHelpers
|
|
28
|
+
onChange={onChange}
|
|
29
|
+
value={undefined}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const input = screen.getByLabelText('Price') as HTMLInputElement;
|
|
34
|
+
fireEvent.focus(input);
|
|
35
|
+
fireEvent.change(input, {
|
|
36
|
+
target: { selectionStart: 4, value: '9.99' },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(onChange).toHaveBeenLastCalledWith(999);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('treats zero-decimal currencies 1:1', () => {
|
|
43
|
+
const onChange = vi.fn();
|
|
44
|
+
render(
|
|
45
|
+
<MoneyInput
|
|
46
|
+
aria-label="Price"
|
|
47
|
+
currency="VND"
|
|
48
|
+
hideHelpers
|
|
49
|
+
onChange={onChange}
|
|
50
|
+
value={25000}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const input = screen.getByLabelText('Price') as HTMLInputElement;
|
|
55
|
+
// 25000 minor units == 25000 major units for VND (0 fraction digits).
|
|
56
|
+
expect(input.value.replace(/[^\d]/g, '')).toBe('25000');
|
|
57
|
+
|
|
58
|
+
fireEvent.focus(input);
|
|
59
|
+
fireEvent.change(input, {
|
|
60
|
+
target: { selectionStart: 3, value: '500' },
|
|
61
|
+
});
|
|
62
|
+
expect(onChange).toHaveBeenLastCalledWith(500);
|
|
63
|
+
});
|
|
64
|
+
});
|