@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
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ShieldUser,
|
|
6
6
|
User as UserIcon,
|
|
7
7
|
UserMinus,
|
|
8
|
+
X,
|
|
8
9
|
} from '@tuturuuu/icons';
|
|
9
10
|
import type { InternalApiEnhancedWorkspaceMember } from '@tuturuuu/types';
|
|
10
11
|
import { Avatar, AvatarFallback, AvatarImage } from '@tuturuuu/ui/avatar';
|
|
@@ -26,6 +27,13 @@ import {
|
|
|
26
27
|
} from './member-filter-utils';
|
|
27
28
|
import type { WorkspaceAccessLabels, WorkspaceAccessRole } from './types';
|
|
28
29
|
|
|
30
|
+
type GuestContext = InternalApiEnhancedWorkspaceMember & {
|
|
31
|
+
direct_board_guest?: boolean;
|
|
32
|
+
guest_board_count?: number;
|
|
33
|
+
guest_board_names?: string[];
|
|
34
|
+
guest_highest_permission?: 'edit' | 'view' | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
29
37
|
type Props = {
|
|
30
38
|
canManageMembers: boolean;
|
|
31
39
|
canManageRoles: boolean;
|
|
@@ -57,191 +65,192 @@ export function WorkspaceAccessMemberRow({
|
|
|
57
65
|
const memberId = member.id ?? null;
|
|
58
66
|
const availableRoles = getAvailableRolesForMember(member, roles);
|
|
59
67
|
const memberName = getMemberDisplayName(member, t('common.unknown'));
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
const guest = member as GuestContext;
|
|
69
|
+
const canRemoveRoles = canManageRoles && memberId && !member.pending;
|
|
70
|
+
|
|
71
|
+
const accent = member.pending
|
|
72
|
+
? 'before:bg-dynamic-orange'
|
|
73
|
+
: guest.direct_board_guest || member.workspace_member_type === 'GUEST'
|
|
74
|
+
? 'before:bg-dynamic-blue'
|
|
75
|
+
: member.is_creator
|
|
76
|
+
? 'before:bg-dynamic-yellow'
|
|
77
|
+
: 'before:bg-transparent';
|
|
66
78
|
|
|
67
79
|
return (
|
|
68
80
|
<article
|
|
69
|
-
className={`relative rounded-
|
|
70
|
-
member.pending
|
|
71
|
-
? 'border-dashed bg-transparent'
|
|
72
|
-
: 'bg-primary-foreground/20 hover:bg-primary-foreground/30'
|
|
73
|
-
}`}
|
|
81
|
+
className={`group relative overflow-hidden rounded-xl border bg-background pl-5 transition-all before:absolute before:inset-y-0 before:left-0 before:w-1 hover:border-foreground/20 hover:shadow-sm ${accent} ${member.pending ? 'border-dashed' : ''}`}
|
|
74
82
|
>
|
|
75
|
-
<div className="
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<div className="min-w-0 flex-1 space-y-2">
|
|
88
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
89
|
-
<p className="truncate font-semibold lg:text-lg">{memberName}</p>
|
|
90
|
-
{member.is_creator ? (
|
|
91
|
-
<Badge className="h-5 gap-1 border-dynamic-yellow/50 bg-dynamic-yellow/10 px-1.5 text-dynamic-yellow text-xs">
|
|
92
|
-
<Crown className="h-3 w-3" />
|
|
93
|
-
{t('ws-members.creator_badge')}
|
|
94
|
-
</Badge>
|
|
95
|
-
) : null}
|
|
96
|
-
{member.pending ? (
|
|
97
|
-
<Badge variant="outline" className="h-5 px-1.5 text-xs">
|
|
98
|
-
{t('ws-members.invited')}
|
|
99
|
-
</Badge>
|
|
100
|
-
) : null}
|
|
101
|
-
{member.workspace_member_type === 'GUEST' ? (
|
|
102
|
-
<Badge className="h-5 border-foreground/20 bg-foreground/5 px-1.5 text-foreground/80 text-xs">
|
|
103
|
-
{guestContext.direct_board_guest
|
|
104
|
-
? t('ws-members.direct_board_guest_badge')
|
|
105
|
-
: t('ws-members.guest_badge')}
|
|
106
|
-
</Badge>
|
|
107
|
-
) : null}
|
|
108
|
-
{guestContext.direct_board_guest ? (
|
|
109
|
-
<Badge className="h-5 border-dynamic-blue/50 bg-dynamic-blue/10 px-1.5 text-dynamic-blue text-xs">
|
|
110
|
-
{guestContext.guest_highest_permission === 'edit'
|
|
111
|
-
? t('ws-members.board_guest_can_edit')
|
|
112
|
-
: t('ws-members.board_guest_can_view')}
|
|
113
|
-
</Badge>
|
|
114
|
-
) : null}
|
|
115
|
-
</div>
|
|
116
|
-
|
|
117
|
-
<p className="truncate font-semibold text-muted-foreground text-sm">
|
|
118
|
-
{member.email ||
|
|
119
|
-
(member.handle ? `@${member.handle}` : t('common.unknown'))}
|
|
120
|
-
</p>
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
83
|
+
<div className="p-4 pl-0">
|
|
84
|
+
<div className="flex items-start gap-3">
|
|
85
|
+
<Avatar className="h-11 w-11 border border-border">
|
|
86
|
+
<AvatarImage src={member.avatar_url ?? undefined} />
|
|
87
|
+
<AvatarFallback className="font-semibold">
|
|
88
|
+
{member.display_name ? (
|
|
89
|
+
getInitials(member.display_name)
|
|
90
|
+
) : (
|
|
91
|
+
<UserIcon className="h-5 w-5" />
|
|
92
|
+
)}
|
|
93
|
+
</AvatarFallback>
|
|
94
|
+
</Avatar>
|
|
123
95
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
{canManageRoles && memberId && !member.pending ? (
|
|
133
|
-
<button
|
|
134
|
-
type="button"
|
|
135
|
-
className="rounded-full px-1 hover:bg-foreground/10"
|
|
136
|
-
onClick={() =>
|
|
137
|
-
onRemoveRole({
|
|
138
|
-
roleId: role.id,
|
|
139
|
-
userId: memberId,
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
|
-
aria-label={t('common.delete')}
|
|
143
|
-
>
|
|
144
|
-
x
|
|
145
|
-
</button>
|
|
96
|
+
<div className="min-w-0 flex-1">
|
|
97
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
98
|
+
<p className="truncate font-semibold text-base">{memberName}</p>
|
|
99
|
+
{member.is_creator ? (
|
|
100
|
+
<Badge className="h-5 gap-1 border-dynamic-yellow/50 bg-dynamic-yellow/10 px-1.5 text-dynamic-yellow text-xs">
|
|
101
|
+
<Crown className="h-3 w-3" />
|
|
102
|
+
{t('ws-members.creator_badge')}
|
|
103
|
+
</Badge>
|
|
146
104
|
) : null}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<Badge variant="outline" className="h-5 px-1.5 text-xs">
|
|
151
|
-
{labels.noRolesLabel}
|
|
152
|
-
</Badge>
|
|
153
|
-
)}
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
{guestContext.direct_board_guest ? (
|
|
157
|
-
<div className="mt-3 rounded-md border border-dynamic-blue/20 bg-dynamic-blue/5 p-3 text-sm">
|
|
158
|
-
<div className="font-medium text-dynamic-blue">
|
|
159
|
-
{t('ws-members.direct_board_guest_scope_title')}
|
|
160
|
-
</div>
|
|
161
|
-
<p className="mt-1 text-muted-foreground">
|
|
162
|
-
{t('ws-members.direct_board_guest_scope_description', {
|
|
163
|
-
count: guestContext.guest_board_count ?? 0,
|
|
164
|
-
})}
|
|
165
|
-
</p>
|
|
166
|
-
{guestContext.guest_board_names?.length ? (
|
|
167
|
-
<div className="mt-2 flex flex-wrap gap-1">
|
|
168
|
-
{guestContext.guest_board_names.slice(0, 4).map((name) => (
|
|
169
|
-
<Badge key={name} variant="outline" className="text-xs">
|
|
170
|
-
{name}
|
|
105
|
+
{member.pending ? (
|
|
106
|
+
<Badge className="h-5 border-dynamic-orange/50 bg-dynamic-orange/10 px-1.5 text-dynamic-orange text-xs">
|
|
107
|
+
{t('ws-members.invited')}
|
|
171
108
|
</Badge>
|
|
172
|
-
)
|
|
173
|
-
{
|
|
174
|
-
<Badge
|
|
175
|
-
|
|
109
|
+
) : null}
|
|
110
|
+
{member.workspace_member_type === 'GUEST' ? (
|
|
111
|
+
<Badge className="h-5 border-foreground/20 bg-foreground/5 px-1.5 text-foreground/80 text-xs">
|
|
112
|
+
{guest.direct_board_guest
|
|
113
|
+
? t('ws-members.direct_board_guest_badge')
|
|
114
|
+
: t('ws-members.guest_badge')}
|
|
115
|
+
</Badge>
|
|
116
|
+
) : null}
|
|
117
|
+
{guest.direct_board_guest ? (
|
|
118
|
+
<Badge className="h-5 border-dynamic-blue/50 bg-dynamic-blue/10 px-1.5 text-dynamic-blue text-xs">
|
|
119
|
+
{guest.guest_highest_permission === 'edit'
|
|
120
|
+
? t('ws-members.board_guest_can_edit')
|
|
121
|
+
: t('ws-members.board_guest_can_view')}
|
|
176
122
|
</Badge>
|
|
177
123
|
) : null}
|
|
178
124
|
</div>
|
|
179
|
-
|
|
125
|
+
<p className="mt-0.5 truncate text-muted-foreground text-sm">
|
|
126
|
+
{member.email ||
|
|
127
|
+
(member.handle ? `@${member.handle}` : t('common.unknown'))}
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
180
130
|
</div>
|
|
181
|
-
) : null}
|
|
182
131
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
132
|
+
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
133
|
+
{member.roles.length > 0 ? (
|
|
134
|
+
member.roles.map((role) => (
|
|
135
|
+
<Badge
|
|
136
|
+
key={`${member.id ?? member.email}-${role.id}`}
|
|
137
|
+
className="h-5 gap-1 border-dynamic-purple/40 bg-dynamic-purple/10 px-1.5 text-dynamic-purple text-xs"
|
|
138
|
+
>
|
|
139
|
+
<span>{role.name}</span>
|
|
140
|
+
{canRemoveRoles ? (
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
className="rounded-full p-0.5 transition-colors hover:bg-dynamic-purple/20"
|
|
144
|
+
onClick={() =>
|
|
145
|
+
onRemoveRole({ roleId: role.id, userId: memberId })
|
|
146
|
+
}
|
|
147
|
+
aria-label={t('common.delete')}
|
|
148
|
+
>
|
|
149
|
+
<X className="h-3 w-3" />
|
|
150
|
+
</button>
|
|
151
|
+
) : null}
|
|
152
|
+
</Badge>
|
|
153
|
+
))
|
|
154
|
+
) : (
|
|
155
|
+
<Badge variant="outline" className="h-5 px-1.5 text-xs">
|
|
156
|
+
{labels.noRolesLabel}
|
|
157
|
+
</Badge>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{guest.direct_board_guest ? (
|
|
162
|
+
<div className="mt-3 rounded-lg border border-dynamic-blue/20 bg-dynamic-blue/5 p-3 text-sm">
|
|
163
|
+
<div className="font-medium text-dynamic-blue">
|
|
164
|
+
{t('ws-members.direct_board_guest_scope_title')}
|
|
165
|
+
</div>
|
|
166
|
+
<p className="mt-1 text-muted-foreground">
|
|
167
|
+
{t('ws-members.direct_board_guest_scope_description', {
|
|
168
|
+
count: guest.guest_board_count ?? 0,
|
|
169
|
+
})}
|
|
170
|
+
</p>
|
|
171
|
+
{guest.guest_board_names?.length ? (
|
|
172
|
+
<div className="mt-2 flex flex-wrap gap-1">
|
|
173
|
+
{guest.guest_board_names.slice(0, 4).map((name) => (
|
|
174
|
+
<Badge key={name} variant="outline" className="text-xs">
|
|
175
|
+
{name}
|
|
176
|
+
</Badge>
|
|
177
|
+
))}
|
|
178
|
+
{guest.guest_board_names.length > 4 ? (
|
|
179
|
+
<Badge variant="outline" className="text-xs">
|
|
180
|
+
+{guest.guest_board_names.length - 4}
|
|
181
|
+
</Badge>
|
|
182
|
+
) : null}
|
|
183
|
+
</div>
|
|
184
|
+
) : null}
|
|
196
185
|
</div>
|
|
197
186
|
) : null}
|
|
198
187
|
|
|
199
|
-
<div className="flex flex-col gap-
|
|
200
|
-
{
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
<SelectValue placeholder={labels.assignRolePlaceholder} />
|
|
208
|
-
</SelectTrigger>
|
|
209
|
-
<SelectContent>
|
|
210
|
-
{availableRoles.length === 0 ? (
|
|
211
|
-
<SelectItem value="__no_roles__" disabled>
|
|
212
|
-
{labels.noAdditionalRoles}
|
|
213
|
-
</SelectItem>
|
|
214
|
-
) : (
|
|
215
|
-
availableRoles.map((role) => (
|
|
216
|
-
<SelectItem key={role.id} value={role.id}>
|
|
217
|
-
{role.name}
|
|
218
|
-
</SelectItem>
|
|
219
|
-
))
|
|
188
|
+
<div className="mt-3 flex flex-col gap-3 border-border border-t pt-3 text-sm md:flex-row md:items-center md:justify-between">
|
|
189
|
+
{member.created_at ? (
|
|
190
|
+
<div className="text-muted-foreground text-xs">
|
|
191
|
+
<span>
|
|
192
|
+
{t(
|
|
193
|
+
member.pending
|
|
194
|
+
? 'ws-members.invited'
|
|
195
|
+
: 'ws-members.member_since'
|
|
220
196
|
)}
|
|
221
|
-
</
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{canManageMembers && !member.is_creator ? (
|
|
226
|
-
<Button
|
|
227
|
-
variant="outline"
|
|
228
|
-
disabled={isMutating}
|
|
229
|
-
onClick={() =>
|
|
230
|
-
onRemoveMember({
|
|
231
|
-
email: member.email,
|
|
232
|
-
userId: member.pending ? null : member.id,
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
>
|
|
236
|
-
<UserMinus className="mr-2 h-4 w-4" />
|
|
237
|
-
{labels.removeMemberAction}
|
|
238
|
-
</Button>
|
|
239
|
-
) : (
|
|
240
|
-
<div className="inline-flex items-center gap-2 rounded-md border px-3 py-2 text-muted-foreground text-sm">
|
|
241
|
-
<ShieldUser className="h-4 w-4" />
|
|
242
|
-
{labels.protectedMemberLabel}
|
|
197
|
+
</span>{' '}
|
|
198
|
+
<span className="font-semibold text-foreground">
|
|
199
|
+
{moment(member.created_at).locale(locale).fromNow()}
|
|
200
|
+
</span>
|
|
243
201
|
</div>
|
|
202
|
+
) : (
|
|
203
|
+
<span />
|
|
244
204
|
)}
|
|
205
|
+
|
|
206
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
207
|
+
{canRemoveRoles ? (
|
|
208
|
+
<Select
|
|
209
|
+
onValueChange={(roleId) =>
|
|
210
|
+
onAssignRole({ roleId, userId: memberId })
|
|
211
|
+
}
|
|
212
|
+
>
|
|
213
|
+
<SelectTrigger className="h-9 min-w-[180px]">
|
|
214
|
+
<SelectValue placeholder={labels.assignRolePlaceholder} />
|
|
215
|
+
</SelectTrigger>
|
|
216
|
+
<SelectContent>
|
|
217
|
+
{availableRoles.length === 0 ? (
|
|
218
|
+
<SelectItem value="__no_roles__" disabled>
|
|
219
|
+
{labels.noAdditionalRoles}
|
|
220
|
+
</SelectItem>
|
|
221
|
+
) : (
|
|
222
|
+
availableRoles.map((role) => (
|
|
223
|
+
<SelectItem key={role.id} value={role.id}>
|
|
224
|
+
{role.name}
|
|
225
|
+
</SelectItem>
|
|
226
|
+
))
|
|
227
|
+
)}
|
|
228
|
+
</SelectContent>
|
|
229
|
+
</Select>
|
|
230
|
+
) : null}
|
|
231
|
+
|
|
232
|
+
{canManageMembers && !member.is_creator ? (
|
|
233
|
+
<Button
|
|
234
|
+
variant="outline"
|
|
235
|
+
size="sm"
|
|
236
|
+
disabled={isMutating}
|
|
237
|
+
onClick={() =>
|
|
238
|
+
onRemoveMember({
|
|
239
|
+
email: member.email,
|
|
240
|
+
userId: member.pending ? null : member.id,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
>
|
|
244
|
+
<UserMinus className="mr-2 h-4 w-4" />
|
|
245
|
+
{labels.removeMemberAction}
|
|
246
|
+
</Button>
|
|
247
|
+
) : (
|
|
248
|
+
<div className="inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 text-muted-foreground text-xs">
|
|
249
|
+
<ShieldUser className="h-3.5 w-3.5" />
|
|
250
|
+
{labels.protectedMemberLabel}
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
245
254
|
</div>
|
|
246
255
|
</div>
|
|
247
256
|
</article>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { UsersRound } from '@tuturuuu/icons';
|
|
3
4
|
import type { InternalApiEnhancedWorkspaceMember } from '@tuturuuu/types';
|
|
4
5
|
import { Skeleton } from '@tuturuuu/ui/skeleton';
|
|
5
6
|
import { useTranslations } from 'next-intl';
|
|
@@ -43,22 +44,27 @@ export function WorkspaceAccessMembers({
|
|
|
43
44
|
if (isLoading) {
|
|
44
45
|
return (
|
|
45
46
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
46
|
-
<Skeleton className="h-
|
|
47
|
-
<Skeleton className="h-
|
|
48
|
-
<Skeleton className="h-
|
|
49
|
-
<Skeleton className="h-
|
|
47
|
+
<Skeleton className="h-48 rounded-xl" />
|
|
48
|
+
<Skeleton className="h-48 rounded-xl" />
|
|
49
|
+
<Skeleton className="h-48 rounded-xl" />
|
|
50
|
+
<Skeleton className="h-48 rounded-xl" />
|
|
50
51
|
</div>
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
if (members.length === 0) {
|
|
55
56
|
return (
|
|
56
|
-
<div className="flex min-h-56 items-center justify-center rounded-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
<div className="flex min-h-56 flex-col items-center justify-center gap-3 rounded-xl border border-border border-dashed p-8 text-center">
|
|
58
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-foreground/5 text-muted-foreground">
|
|
59
|
+
<UsersRound className="h-6 w-6" />
|
|
60
|
+
</div>
|
|
61
|
+
<p className="text-muted-foreground text-sm">
|
|
62
|
+
{searchTerm.trim()
|
|
63
|
+
? t('ws-members.no_members_match')
|
|
64
|
+
: status === 'invited'
|
|
65
|
+
? t('ws-members.no_invited_members_found')
|
|
66
|
+
: t('ws-members.no_members_found')}
|
|
67
|
+
</p>
|
|
62
68
|
</div>
|
|
63
69
|
);
|
|
64
70
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { ShieldCheck, Users } from '@tuturuuu/icons';
|
|
4
|
-
import { Badge } from '@tuturuuu/ui/badge';
|
|
3
|
+
import { ShieldCheck, UserCheck, UserPlus, Users } from '@tuturuuu/icons';
|
|
5
4
|
import { useTranslations } from 'next-intl';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
6
|
import type { WorkspaceAccessContext, WorkspaceAccessMode } from './types';
|
|
7
7
|
|
|
8
8
|
type Props = {
|
|
@@ -13,6 +13,13 @@ type Props = {
|
|
|
13
13
|
totalCount: number;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
type Stat = {
|
|
17
|
+
accent: string;
|
|
18
|
+
icon: ReactNode;
|
|
19
|
+
label: string;
|
|
20
|
+
value: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
16
23
|
export function WorkspaceAccessPageHeader({
|
|
17
24
|
context,
|
|
18
25
|
invitedCount,
|
|
@@ -22,49 +29,81 @@ export function WorkspaceAccessPageHeader({
|
|
|
22
29
|
}: Props) {
|
|
23
30
|
const t = useTranslations() as (key: string) => string;
|
|
24
31
|
|
|
32
|
+
const stats: Stat[] = [
|
|
33
|
+
{
|
|
34
|
+
accent: 'text-dynamic-blue',
|
|
35
|
+
icon: <Users className="h-4 w-4" />,
|
|
36
|
+
label: t('common.total'),
|
|
37
|
+
value: totalCount,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
accent: 'text-dynamic-green',
|
|
41
|
+
icon: <UserCheck className="h-4 w-4" />,
|
|
42
|
+
label: t('ws-members.active_members'),
|
|
43
|
+
value: joinedCount,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
accent: 'text-dynamic-orange',
|
|
47
|
+
icon: <UserPlus className="h-4 w-4" />,
|
|
48
|
+
label: t('ws-members.pending_invitations'),
|
|
49
|
+
value: invitedCount,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
25
53
|
return (
|
|
26
|
-
<section className="
|
|
27
|
-
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
|
|
28
|
-
<div className="min-w-0">
|
|
29
|
-
<div className="flex items-center
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<div className="
|
|
34
|
-
<h1 className="
|
|
54
|
+
<section className="rounded-xl border border-border bg-background">
|
|
55
|
+
<div className="flex flex-col gap-6 p-6 lg:flex-row lg:items-start lg:justify-between">
|
|
56
|
+
<div className="flex min-w-0 gap-4">
|
|
57
|
+
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-dynamic-blue/30 bg-dynamic-blue/10 text-dynamic-blue">
|
|
58
|
+
<Users className="h-5 w-5" />
|
|
59
|
+
</div>
|
|
60
|
+
<div className="min-w-0">
|
|
61
|
+
<div className="flex items-center gap-2">
|
|
62
|
+
<h1 className="truncate font-bold text-2xl tracking-tight">
|
|
35
63
|
{mode === 'cms'
|
|
36
64
|
? t('external-projects.settings.members_title')
|
|
37
65
|
: t('workspace-settings-layout.members')}
|
|
38
66
|
</h1>
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
</p>
|
|
44
|
-
) : null}
|
|
67
|
+
<span className="inline-flex items-center gap-1 rounded-full border border-dynamic-purple/30 bg-dynamic-purple/10 px-2 py-0.5 font-medium text-dynamic-purple text-xs">
|
|
68
|
+
<ShieldCheck className="h-3 w-3" />
|
|
69
|
+
{t('ws-roles.plural')}
|
|
70
|
+
</span>
|
|
45
71
|
</div>
|
|
72
|
+
<p className="mt-1.5 max-w-2xl text-muted-foreground text-sm leading-6">
|
|
73
|
+
{mode === 'cms'
|
|
74
|
+
? t('external-projects.settings.members_description')
|
|
75
|
+
: t('ws-members.description')}
|
|
76
|
+
</p>
|
|
77
|
+
{context.boundProjectName ? (
|
|
78
|
+
<p className="mt-2 inline-flex items-center gap-1.5 rounded-md border border-border bg-foreground/[0.03] px-2 py-1 text-muted-foreground text-xs">
|
|
79
|
+
{t('external-projects.settings.bound_project_label')}:
|
|
80
|
+
<span className="font-semibold text-foreground">
|
|
81
|
+
{context.boundProjectName}
|
|
82
|
+
</span>
|
|
83
|
+
</p>
|
|
84
|
+
) : null}
|
|
46
85
|
</div>
|
|
47
|
-
<p className="mt-4 max-w-3xl text-muted-foreground text-sm leading-6">
|
|
48
|
-
{mode === 'cms'
|
|
49
|
-
? t('external-projects.settings.members_description')
|
|
50
|
-
: t('ws-members.description')}
|
|
51
|
-
</p>
|
|
52
86
|
</div>
|
|
53
87
|
|
|
54
|
-
<div className="
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
88
|
+
<div className="grid shrink-0 grid-cols-3 divide-x divide-border overflow-hidden rounded-lg border border-border bg-foreground/[0.02]">
|
|
89
|
+
{stats.map((stat) => (
|
|
90
|
+
<div
|
|
91
|
+
key={stat.label}
|
|
92
|
+
className="flex flex-col gap-1 px-4 py-3 text-center"
|
|
93
|
+
>
|
|
94
|
+
<span
|
|
95
|
+
className={`inline-flex items-center justify-center gap-1.5 ${stat.accent}`}
|
|
96
|
+
>
|
|
97
|
+
{stat.icon}
|
|
98
|
+
<span className="font-bold text-foreground text-xl tabular-nums">
|
|
99
|
+
{stat.value}
|
|
100
|
+
</span>
|
|
101
|
+
</span>
|
|
102
|
+
<span className="text-muted-foreground text-xs">
|
|
103
|
+
{stat.label}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
68
107
|
</div>
|
|
69
108
|
</div>
|
|
70
109
|
</section>
|