@tuturuuu/ui 0.9.0 → 0.10.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 +29 -0
- package/package.json +6 -5
- package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +27 -1
- package/src/components/ui/custom/nav-link.test.tsx +165 -0
- package/src/components/ui/custom/nav-link.tsx +69 -11
- package/src/components/ui/custom/navigation.tsx +1 -0
- package/src/components/ui/custom/settings/task-settings.tsx +104 -0
- package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
- package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
- package/src/components/ui/custom/settings-dialog-search.ts +75 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +63 -27
- package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
- package/src/components/ui/custom/workspace-select.tsx +17 -16
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +16 -0
- package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +4 -328
- package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +50 -37
- package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +263 -21
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +133 -14
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +112 -54
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +8 -2
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +29 -14
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +24 -1
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +7 -1
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +80 -8
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +15 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
- package/src/components/ui/tu-do/boards/form.tsx +1 -1
- package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
- package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
- package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
- package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
- package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
- package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +116 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +38 -0
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +128 -7
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +222 -9
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +21 -0
- package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
- package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +14 -4
- package/src/components/ui/tu-do/shared/board-header.tsx +8 -1
- package/src/components/ui/tu-do/shared/board-switcher.tsx +70 -38
- package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
- package/src/components/ui/tu-do/shared/board-views.tsx +49 -69
- package/src/components/ui/tu-do/shared/list-view.tsx +21 -3
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
- package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +9 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
- package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
- package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
- package/src/hooks/useBoardPresence.ts +364 -0
- package/src/hooks/useBoardRealtimeEventHandler.ts +34 -90
- package/src/lib/workspace-actions.ts +2 -6
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
import { Loader2, Trash2, Users } from '@tuturuuu/icons';
|
|
5
|
+
import {
|
|
6
|
+
createWorkspaceTaskBoardShare,
|
|
7
|
+
deleteWorkspaceTaskBoardShare,
|
|
8
|
+
listWorkspaceTaskBoardShares,
|
|
9
|
+
listWorkspaceTaskBoardViewableMembers,
|
|
10
|
+
updateWorkspaceTaskBoardShare,
|
|
11
|
+
type WorkspaceTaskBoardShare,
|
|
12
|
+
type WorkspaceTaskBoardSharePermission,
|
|
13
|
+
type WorkspaceTaskBoardViewableMember,
|
|
14
|
+
} from '@tuturuuu/internal-api/tasks';
|
|
15
|
+
import type { WorkspaceTaskBoard } from '@tuturuuu/types';
|
|
16
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@tuturuuu/ui/avatar';
|
|
17
|
+
import { Badge } from '@tuturuuu/ui/badge';
|
|
18
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
19
|
+
import { Combobox } from '@tuturuuu/ui/custom/combobox';
|
|
20
|
+
import { Input } from '@tuturuuu/ui/input';
|
|
21
|
+
import { getInitials } from '@tuturuuu/utils/name-helper';
|
|
22
|
+
import { useTranslations } from 'next-intl';
|
|
23
|
+
import { useState } from 'react';
|
|
24
|
+
import { toast } from 'sonner';
|
|
25
|
+
import { BoardPublicLinkSection } from './board-public-link-section';
|
|
26
|
+
import { ShareSection } from './share-section';
|
|
27
|
+
|
|
28
|
+
interface BoardShareSettingsPanelProps {
|
|
29
|
+
board: Pick<WorkspaceTaskBoard, 'id' | 'name'>;
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
wsId: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shareDisplayName(share: WorkspaceTaskBoardShare) {
|
|
35
|
+
return (
|
|
36
|
+
share.user?.display_name ||
|
|
37
|
+
(share.user?.handle ? `@${share.user.handle}` : null) ||
|
|
38
|
+
share.email ||
|
|
39
|
+
share.user_id ||
|
|
40
|
+
'Guest'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function viewableMemberDisplayName(member: WorkspaceTaskBoardViewableMember) {
|
|
45
|
+
return (
|
|
46
|
+
member.display_name ||
|
|
47
|
+
(member.handle ? `@${member.handle}` : null) ||
|
|
48
|
+
member.email ||
|
|
49
|
+
member.user_id
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function BoardShareSettingsPanel({
|
|
54
|
+
board,
|
|
55
|
+
enabled = true,
|
|
56
|
+
wsId,
|
|
57
|
+
}: BoardShareSettingsPanelProps) {
|
|
58
|
+
const t = useTranslations();
|
|
59
|
+
const queryClient = useQueryClient();
|
|
60
|
+
const [email, setEmail] = useState('');
|
|
61
|
+
const [permission, setPermission] =
|
|
62
|
+
useState<WorkspaceTaskBoardSharePermission>('view');
|
|
63
|
+
const [membersOpen, setMembersOpen] = useState(false);
|
|
64
|
+
const [guestsOpen, setGuestsOpen] = useState(false);
|
|
65
|
+
|
|
66
|
+
const queryKey = ['task-board-shares', wsId, board.id] as const;
|
|
67
|
+
const sharesQuery = useQuery({
|
|
68
|
+
queryKey,
|
|
69
|
+
queryFn: () => listWorkspaceTaskBoardShares(wsId, board.id),
|
|
70
|
+
enabled,
|
|
71
|
+
});
|
|
72
|
+
const viewableMembersQuery = useQuery({
|
|
73
|
+
queryKey: ['task-board-viewable-members', wsId, board.id] as const,
|
|
74
|
+
queryFn: () => listWorkspaceTaskBoardViewableMembers(wsId, board.id),
|
|
75
|
+
enabled: enabled && membersOpen,
|
|
76
|
+
staleTime: 60_000,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const createMutation = useMutation({
|
|
80
|
+
mutationFn: () =>
|
|
81
|
+
createWorkspaceTaskBoardShare(wsId, board.id, {
|
|
82
|
+
email,
|
|
83
|
+
permission,
|
|
84
|
+
}),
|
|
85
|
+
onSuccess: () => {
|
|
86
|
+
setEmail('');
|
|
87
|
+
setPermission('view');
|
|
88
|
+
void queryClient.invalidateQueries({ queryKey });
|
|
89
|
+
toast.success(t('ws-task-boards.share.saved'));
|
|
90
|
+
},
|
|
91
|
+
onError: () => {
|
|
92
|
+
toast.error(t('common.error'));
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const updateMutation = useMutation({
|
|
97
|
+
mutationFn: ({
|
|
98
|
+
nextPermission,
|
|
99
|
+
shareId,
|
|
100
|
+
}: {
|
|
101
|
+
nextPermission: WorkspaceTaskBoardSharePermission;
|
|
102
|
+
shareId: string;
|
|
103
|
+
}) =>
|
|
104
|
+
updateWorkspaceTaskBoardShare(wsId, board.id, {
|
|
105
|
+
shareId,
|
|
106
|
+
permission: nextPermission,
|
|
107
|
+
}),
|
|
108
|
+
onSuccess: () => {
|
|
109
|
+
void queryClient.invalidateQueries({ queryKey });
|
|
110
|
+
},
|
|
111
|
+
onError: () => {
|
|
112
|
+
toast.error(t('common.error'));
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const deleteMutation = useMutation({
|
|
117
|
+
mutationFn: (shareId: string) =>
|
|
118
|
+
deleteWorkspaceTaskBoardShare(wsId, board.id, shareId),
|
|
119
|
+
onSuccess: () => {
|
|
120
|
+
void queryClient.invalidateQueries({ queryKey });
|
|
121
|
+
toast.success(t('ws-task-boards.share.removed'));
|
|
122
|
+
},
|
|
123
|
+
onError: () => {
|
|
124
|
+
toast.error(t('common.error'));
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const shares = sharesQuery.data?.shares ?? [];
|
|
129
|
+
const members = Array.isArray(viewableMembersQuery.data?.members)
|
|
130
|
+
? viewableMembersQuery.data.members
|
|
131
|
+
: undefined;
|
|
132
|
+
const membersCount = members?.length;
|
|
133
|
+
const membersStatusBadge = viewableMembersQuery.isLoading ? (
|
|
134
|
+
<Badge variant="secondary" className="gap-1 px-2 py-0.5 text-[10px]">
|
|
135
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
136
|
+
{t('common.loading')}
|
|
137
|
+
</Badge>
|
|
138
|
+
) : typeof membersCount === 'number' ? (
|
|
139
|
+
<Badge variant="secondary" className="px-2 py-0.5 text-[10px]">
|
|
140
|
+
{membersCount}
|
|
141
|
+
</Badge>
|
|
142
|
+
) : (
|
|
143
|
+
<Badge variant="outline" className="px-2 py-0.5 text-[10px]">
|
|
144
|
+
{t('common.workspace')}
|
|
145
|
+
</Badge>
|
|
146
|
+
);
|
|
147
|
+
const guestsStatusBadge = sharesQuery.isLoading ? (
|
|
148
|
+
<Badge variant="secondary" className="gap-1 px-2 py-0.5 text-[10px]">
|
|
149
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
150
|
+
{t('common.loading')}
|
|
151
|
+
</Badge>
|
|
152
|
+
) : shares.length ? (
|
|
153
|
+
<Badge variant="secondary" className="px-2 py-0.5 text-[10px]">
|
|
154
|
+
{shares.length}
|
|
155
|
+
</Badge>
|
|
156
|
+
) : (
|
|
157
|
+
<Badge variant="outline" className="px-2 py-0.5 text-[10px]">
|
|
158
|
+
{t('common.none')}
|
|
159
|
+
</Badge>
|
|
160
|
+
);
|
|
161
|
+
const canSubmit =
|
|
162
|
+
email.trim().length > 0 &&
|
|
163
|
+
!createMutation.isPending &&
|
|
164
|
+
!sharesQuery.isLoading;
|
|
165
|
+
const permissionOptions = [
|
|
166
|
+
{
|
|
167
|
+
value: 'view',
|
|
168
|
+
label: t('ws-task-boards.share.permission.view'),
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
value: 'edit',
|
|
172
|
+
label: t('ws-task-boards.share.permission.edit'),
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div className="space-y-2">
|
|
178
|
+
<BoardPublicLinkSection boardId={board.id} open={enabled} wsId={wsId} />
|
|
179
|
+
|
|
180
|
+
<ShareSection
|
|
181
|
+
open={membersOpen}
|
|
182
|
+
onOpenChange={setMembersOpen}
|
|
183
|
+
title={t('ws-task-boards.share.workspace_members.title')}
|
|
184
|
+
tooltip={t('ws-task-boards.share.workspace_members.tooltip')}
|
|
185
|
+
icon={<Users className="h-4 w-4 text-muted-foreground" />}
|
|
186
|
+
statusBadge={membersStatusBadge}
|
|
187
|
+
>
|
|
188
|
+
{viewableMembersQuery.isLoading ? (
|
|
189
|
+
<div className="flex items-center gap-2 text-muted-foreground text-sm">
|
|
190
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
191
|
+
{t('common.loading')}
|
|
192
|
+
</div>
|
|
193
|
+
) : (members ?? []).length === 0 ? (
|
|
194
|
+
<div className="text-muted-foreground text-sm">
|
|
195
|
+
{t('ws-task-boards.share.workspace_members.empty')}
|
|
196
|
+
</div>
|
|
197
|
+
) : (
|
|
198
|
+
<div className="divide-y rounded-md border">
|
|
199
|
+
{members?.map((member) => (
|
|
200
|
+
<div
|
|
201
|
+
key={member.user_id}
|
|
202
|
+
className="flex flex-col gap-3 p-3 sm:flex-row sm:items-center"
|
|
203
|
+
>
|
|
204
|
+
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
205
|
+
<Avatar className="h-8 w-8">
|
|
206
|
+
<AvatarImage src={member.avatar_url ?? undefined} />
|
|
207
|
+
<AvatarFallback>
|
|
208
|
+
{getInitials(viewableMemberDisplayName(member))}
|
|
209
|
+
</AvatarFallback>
|
|
210
|
+
</Avatar>
|
|
211
|
+
<div className="min-w-0">
|
|
212
|
+
<div className="truncate font-medium text-sm">
|
|
213
|
+
{viewableMemberDisplayName(member)}
|
|
214
|
+
</div>
|
|
215
|
+
<div className="truncate text-muted-foreground text-xs">
|
|
216
|
+
{member.email || member.user_id}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
221
|
+
{member.is_creator && (
|
|
222
|
+
<Badge variant="secondary">
|
|
223
|
+
{t('ws-task-boards.share.workspace_members.creator')}
|
|
224
|
+
</Badge>
|
|
225
|
+
)}
|
|
226
|
+
{member.roles.slice(0, 2).map((role) => (
|
|
227
|
+
<Badge key={role.id} variant="outline">
|
|
228
|
+
{role.name}
|
|
229
|
+
</Badge>
|
|
230
|
+
))}
|
|
231
|
+
<Badge variant="outline">
|
|
232
|
+
{t('ws-task-boards.share.workspace_members.badge')}
|
|
233
|
+
</Badge>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</ShareSection>
|
|
240
|
+
|
|
241
|
+
<ShareSection
|
|
242
|
+
open={guestsOpen}
|
|
243
|
+
onOpenChange={setGuestsOpen}
|
|
244
|
+
title={t('ws-task-boards.share.guests.title')}
|
|
245
|
+
tooltip={t('ws-task-boards.share.guests.tooltip')}
|
|
246
|
+
icon={<Users className="h-4 w-4 text-muted-foreground" />}
|
|
247
|
+
statusBadge={guestsStatusBadge}
|
|
248
|
+
>
|
|
249
|
+
<div className="space-y-3">
|
|
250
|
+
<div className="grid gap-2 sm:grid-cols-[1fr_8rem_auto]">
|
|
251
|
+
<Input
|
|
252
|
+
type="email"
|
|
253
|
+
value={email}
|
|
254
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
255
|
+
placeholder={t('ws-task-boards.share.email_placeholder')}
|
|
256
|
+
/>
|
|
257
|
+
<Combobox
|
|
258
|
+
mode="single"
|
|
259
|
+
options={permissionOptions}
|
|
260
|
+
selected={permission}
|
|
261
|
+
onChange={(value) =>
|
|
262
|
+
setPermission(value as WorkspaceTaskBoardSharePermission)
|
|
263
|
+
}
|
|
264
|
+
placeholder={t('ws-task-boards.share.permission.view')}
|
|
265
|
+
searchPlaceholder={t('common.search_members')}
|
|
266
|
+
className="[&_button]:h-9"
|
|
267
|
+
/>
|
|
268
|
+
<Button
|
|
269
|
+
type="button"
|
|
270
|
+
onClick={() => createMutation.mutate()}
|
|
271
|
+
disabled={!canSubmit}
|
|
272
|
+
>
|
|
273
|
+
{createMutation.isPending ? (
|
|
274
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
275
|
+
) : (
|
|
276
|
+
t('common.share')
|
|
277
|
+
)}
|
|
278
|
+
</Button>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{sharesQuery.isLoading ? (
|
|
282
|
+
<div className="rounded-md border border-dashed p-4 text-muted-foreground text-sm">
|
|
283
|
+
{t('common.loading')}
|
|
284
|
+
</div>
|
|
285
|
+
) : shares.length === 0 ? (
|
|
286
|
+
<div className="rounded-md border border-dashed p-4 text-muted-foreground text-sm">
|
|
287
|
+
{t('ws-task-boards.share.empty')}
|
|
288
|
+
</div>
|
|
289
|
+
) : (
|
|
290
|
+
<div className="divide-y rounded-md border">
|
|
291
|
+
{shares.map((share) => (
|
|
292
|
+
<div
|
|
293
|
+
key={share.id}
|
|
294
|
+
className="flex flex-col gap-3 p-3 sm:flex-row sm:items-center"
|
|
295
|
+
>
|
|
296
|
+
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
297
|
+
<Avatar className="h-8 w-8">
|
|
298
|
+
<AvatarImage src={share.user?.avatar_url ?? undefined} />
|
|
299
|
+
<AvatarFallback>
|
|
300
|
+
{getInitials(shareDisplayName(share))}
|
|
301
|
+
</AvatarFallback>
|
|
302
|
+
</Avatar>
|
|
303
|
+
<div className="min-w-0">
|
|
304
|
+
<div className="truncate font-medium text-sm">
|
|
305
|
+
{shareDisplayName(share)}
|
|
306
|
+
</div>
|
|
307
|
+
<div className="truncate text-muted-foreground text-xs">
|
|
308
|
+
{share.email || share.user_id}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<Badge variant="outline" className="w-fit">
|
|
314
|
+
{t('common.guest_access')}
|
|
315
|
+
</Badge>
|
|
316
|
+
|
|
317
|
+
<Combobox
|
|
318
|
+
mode="single"
|
|
319
|
+
options={permissionOptions}
|
|
320
|
+
selected={share.permission}
|
|
321
|
+
onChange={(value) =>
|
|
322
|
+
updateMutation.mutate({
|
|
323
|
+
shareId: share.id,
|
|
324
|
+
nextPermission:
|
|
325
|
+
value as WorkspaceTaskBoardSharePermission,
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
placeholder={t('ws-task-boards.share.permission.view')}
|
|
329
|
+
searchPlaceholder={t('common.search_members')}
|
|
330
|
+
className="w-28 [&_button]:h-9"
|
|
331
|
+
/>
|
|
332
|
+
|
|
333
|
+
<Button
|
|
334
|
+
type="button"
|
|
335
|
+
variant="ghost"
|
|
336
|
+
size="icon"
|
|
337
|
+
onClick={() => deleteMutation.mutate(share.id)}
|
|
338
|
+
disabled={deleteMutation.isPending}
|
|
339
|
+
aria-label={t('common.remove')}
|
|
340
|
+
>
|
|
341
|
+
<Trash2 className="h-4 w-4" />
|
|
342
|
+
</Button>
|
|
343
|
+
</div>
|
|
344
|
+
))}
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
</div>
|
|
348
|
+
</ShareSection>
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from './kanban/kanban-column-collapse';
|
|
52
52
|
import { ListActions } from './list-actions';
|
|
53
53
|
import { statusIcons } from './status-section';
|
|
54
|
+
import type { TaskCardAssigneeMemberSource } from './task-card/task-card';
|
|
54
55
|
import type { TaskFilters } from './task-filter';
|
|
55
56
|
import { VirtualizedTaskList } from './task-list';
|
|
56
57
|
|
|
@@ -163,6 +164,8 @@ interface BoardColumnProps {
|
|
|
163
164
|
isMultiSelectMode?: boolean;
|
|
164
165
|
setIsMultiSelectMode?: (value: boolean) => void;
|
|
165
166
|
isPersonalWorkspace?: boolean;
|
|
167
|
+
canUseBoardAssignees?: boolean;
|
|
168
|
+
assigneeMemberSource?: TaskCardAssigneeMemberSource;
|
|
166
169
|
onTaskSelect?: (taskId: string, event: React.MouseEvent) => void;
|
|
167
170
|
onClearSelection?: () => void;
|
|
168
171
|
onAddTask?: (list: TaskList) => void;
|
|
@@ -177,6 +180,7 @@ interface BoardColumnProps {
|
|
|
177
180
|
onExternalTasksCollapsedChange?: (collapsed: boolean) => void;
|
|
178
181
|
onTaskListCollapsedChange?: (listId: string, collapsed: boolean) => void;
|
|
179
182
|
specialPinned?: boolean;
|
|
183
|
+
specialStickyOffset?: string;
|
|
180
184
|
onSpecialPinnedChange?: (pinned: boolean) => void;
|
|
181
185
|
readOnly?: boolean;
|
|
182
186
|
}
|
|
@@ -194,6 +198,8 @@ export function BoardColumn({
|
|
|
194
198
|
isMultiSelectMode,
|
|
195
199
|
setIsMultiSelectMode,
|
|
196
200
|
isPersonalWorkspace,
|
|
201
|
+
canUseBoardAssignees,
|
|
202
|
+
assigneeMemberSource,
|
|
197
203
|
onAddTask,
|
|
198
204
|
dragPreviewPosition,
|
|
199
205
|
suppressTaskTransforms,
|
|
@@ -206,6 +212,7 @@ export function BoardColumn({
|
|
|
206
212
|
onExternalTasksCollapsedChange,
|
|
207
213
|
onTaskListCollapsedChange,
|
|
208
214
|
specialPinned = false,
|
|
215
|
+
specialStickyOffset,
|
|
209
216
|
onSpecialPinnedChange,
|
|
210
217
|
readOnly = false,
|
|
211
218
|
}: BoardColumnProps) {
|
|
@@ -410,6 +417,12 @@ export function BoardColumn({
|
|
|
410
417
|
transform: CSS.Transform.toString(transform),
|
|
411
418
|
transition,
|
|
412
419
|
};
|
|
420
|
+
const columnStyle: React.CSSProperties = specialStickyOffset
|
|
421
|
+
? {
|
|
422
|
+
...style,
|
|
423
|
+
left: `calc(var(--kanban-snap-left-padding) + ${specialStickyOffset})`,
|
|
424
|
+
}
|
|
425
|
+
: style;
|
|
413
426
|
|
|
414
427
|
const handleUpdate = () => {
|
|
415
428
|
onUpdate?.();
|
|
@@ -513,12 +526,14 @@ export function BoardColumn({
|
|
|
513
526
|
return (
|
|
514
527
|
<Card
|
|
515
528
|
ref={composedRef}
|
|
516
|
-
style={
|
|
529
|
+
style={columnStyle}
|
|
530
|
+
data-kanban-pinned-special={specialStickyOffset ? 'true' : undefined}
|
|
517
531
|
data-kanban-column-id={column.id}
|
|
518
532
|
data-kanban-real-column={isExternalStaging ? undefined : 'true'}
|
|
519
533
|
className={cn(
|
|
520
534
|
'group flex h-full w-14 shrink-0 snap-start flex-col items-center rounded-xl border border-dashed transition-all duration-200',
|
|
521
535
|
'touch-none select-none overflow-hidden hover:shadow-md',
|
|
536
|
+
specialStickyOffset && 'sticky z-30',
|
|
522
537
|
isExternalCollapsed
|
|
523
538
|
? 'border-dynamic-cyan/45 bg-dynamic-cyan/[0.035]'
|
|
524
539
|
: colorClass
|
|
@@ -564,12 +579,14 @@ export function BoardColumn({
|
|
|
564
579
|
return (
|
|
565
580
|
<Card
|
|
566
581
|
ref={composedRef}
|
|
567
|
-
style={
|
|
582
|
+
style={columnStyle}
|
|
583
|
+
data-kanban-pinned-special={specialStickyOffset ? 'true' : undefined}
|
|
568
584
|
data-kanban-column-id={column.id}
|
|
569
585
|
data-kanban-real-column={isExternalStaging ? undefined : 'true'}
|
|
570
586
|
className={cn(
|
|
571
587
|
'group flex h-full w-[var(--kanban-column-width)] shrink-0 snap-start flex-col rounded-xl transition-all duration-200 last:snap-end',
|
|
572
588
|
'touch-none select-none',
|
|
589
|
+
specialStickyOffset && 'sticky z-30',
|
|
573
590
|
colorClass,
|
|
574
591
|
isDragging &&
|
|
575
592
|
'rotate-1 scale-[1.02] opacity-90 shadow-xl ring-2 ring-primary/20',
|
|
@@ -742,19 +759,17 @@ export function BoardColumn({
|
|
|
742
759
|
)}
|
|
743
760
|
</Button>
|
|
744
761
|
) : null}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
</Button>
|
|
757
|
-
)}
|
|
762
|
+
<Button
|
|
763
|
+
type="button"
|
|
764
|
+
variant="ghost"
|
|
765
|
+
size="xs"
|
|
766
|
+
className="h-7 w-7 p-0 text-dynamic-cyan hover:bg-dynamic-cyan/10"
|
|
767
|
+
title={tTasks('collapse_external_tasks')}
|
|
768
|
+
aria-label={tTasks('collapse_external_tasks')}
|
|
769
|
+
onClick={() => onExternalTasksCollapsedChange?.(true)}
|
|
770
|
+
>
|
|
771
|
+
<ChevronLeft className="h-3.5 w-3.5" />
|
|
772
|
+
</Button>
|
|
758
773
|
</>
|
|
759
774
|
) : (
|
|
760
775
|
<>
|
|
@@ -781,28 +796,24 @@ export function BoardColumn({
|
|
|
781
796
|
)}
|
|
782
797
|
</Button>
|
|
783
798
|
) : null}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
>
|
|
803
|
-
<ChevronLeft className="h-3.5 w-3.5" />
|
|
804
|
-
</Button>
|
|
805
|
-
)}
|
|
799
|
+
<Button
|
|
800
|
+
type="button"
|
|
801
|
+
variant="ghost"
|
|
802
|
+
size="xs"
|
|
803
|
+
className={cn(
|
|
804
|
+
'h-7 w-7 p-0 hover:bg-muted/40',
|
|
805
|
+
getListTextColorClass(column.color as SupportedColor)
|
|
806
|
+
)}
|
|
807
|
+
title={tTasks('collapse_task_list', {
|
|
808
|
+
name: translateListName(column.name),
|
|
809
|
+
})}
|
|
810
|
+
aria-label={tTasks('collapse_task_list', {
|
|
811
|
+
name: translateListName(column.name),
|
|
812
|
+
})}
|
|
813
|
+
onClick={() => onTaskListCollapsedChange?.(column.id, true)}
|
|
814
|
+
>
|
|
815
|
+
<ChevronLeft className="h-3.5 w-3.5" />
|
|
816
|
+
</Button>
|
|
806
817
|
</>
|
|
807
818
|
) : null}
|
|
808
819
|
{!readOnly && (
|
|
@@ -841,6 +852,8 @@ export function BoardColumn({
|
|
|
841
852
|
isMultiSelectMode={isMultiSelectMode}
|
|
842
853
|
selectedTasks={selectedTasks}
|
|
843
854
|
isPersonalWorkspace={isPersonalWorkspace}
|
|
855
|
+
canUseBoardAssignees={canUseBoardAssignees}
|
|
856
|
+
assigneeMemberSource={assigneeMemberSource}
|
|
844
857
|
onTaskSelect={onTaskSelect}
|
|
845
858
|
onClearSelection={onClearSelection}
|
|
846
859
|
dragPreviewPosition={dragPreviewPosition}
|
|
@@ -44,6 +44,7 @@ import { useBoardBroadcast } from '../../shared/board-broadcast-context';
|
|
|
44
44
|
import { isTaskListNameExistsError } from '../../shared/task-board-errors';
|
|
45
45
|
import { normalizeBoardText } from './board-text-utils';
|
|
46
46
|
import { TaskCard } from './task';
|
|
47
|
+
import type { TaskCardAssigneeMemberSource } from './task-card/task-card';
|
|
47
48
|
|
|
48
49
|
interface Props {
|
|
49
50
|
list: TaskList;
|
|
@@ -54,6 +55,8 @@ interface Props {
|
|
|
54
55
|
isOverlay?: boolean;
|
|
55
56
|
hideTasksMode?: boolean;
|
|
56
57
|
isPersonalWorkspace?: boolean;
|
|
58
|
+
canUseBoardAssignees?: boolean;
|
|
59
|
+
assigneeMemberSource?: TaskCardAssigneeMemberSource;
|
|
57
60
|
onAddTask?: (list: TaskList) => void;
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -102,6 +105,8 @@ export function EnhancedTaskList({
|
|
|
102
105
|
isOverlay = false,
|
|
103
106
|
hideTasksMode = false,
|
|
104
107
|
isPersonalWorkspace = false,
|
|
108
|
+
canUseBoardAssignees,
|
|
109
|
+
assigneeMemberSource,
|
|
105
110
|
onAddTask,
|
|
106
111
|
}: Props) {
|
|
107
112
|
const t = useTranslations('common');
|
|
@@ -479,6 +484,8 @@ export function EnhancedTaskList({
|
|
|
479
484
|
taskList={list}
|
|
480
485
|
boardId={boardId}
|
|
481
486
|
isPersonalWorkspace={isPersonalWorkspace}
|
|
487
|
+
canUseBoardAssignees={canUseBoardAssignees}
|
|
488
|
+
assigneeMemberSource={assigneeMemberSource}
|
|
482
489
|
onUpdate={onUpdate}
|
|
483
490
|
/>
|
|
484
491
|
))}
|
|
@@ -12,9 +12,9 @@ export interface WorkspaceProject {
|
|
|
12
12
|
export interface WorkspaceMember {
|
|
13
13
|
id: string;
|
|
14
14
|
user_id?: string;
|
|
15
|
-
display_name
|
|
16
|
-
email
|
|
17
|
-
avatar_url
|
|
15
|
+
display_name?: string;
|
|
16
|
+
email?: string;
|
|
17
|
+
avatar_url?: string | null;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface BulkOperationsConfig {
|
|
@@ -5,14 +5,24 @@ import {
|
|
|
5
5
|
listWorkspaceLabels,
|
|
6
6
|
listWorkspaceTaskProjects,
|
|
7
7
|
} from '@tuturuuu/internal-api';
|
|
8
|
+
import { listWorkspaceTaskBoardViewableMembers } from '@tuturuuu/internal-api/tasks';
|
|
8
9
|
import type { Workspace } from '@tuturuuu/types';
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
useWorkspaceMembers,
|
|
12
|
+
type WorkspaceMember,
|
|
13
|
+
} from '@tuturuuu/ui/hooks/use-workspace-members';
|
|
10
14
|
|
|
11
15
|
export function useBulkResources({
|
|
16
|
+
boardId,
|
|
17
|
+
canUseBoardAssignees,
|
|
18
|
+
assigneeMemberSource,
|
|
12
19
|
workspace,
|
|
13
20
|
isMultiSelectMode,
|
|
14
21
|
selectedCount,
|
|
15
22
|
}: {
|
|
23
|
+
boardId?: string | null;
|
|
24
|
+
canUseBoardAssignees?: boolean;
|
|
25
|
+
assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
|
|
16
26
|
workspace: Workspace;
|
|
17
27
|
isMultiSelectMode: boolean;
|
|
18
28
|
selectedCount: number;
|
|
@@ -46,13 +56,57 @@ export function useBulkResources({
|
|
|
46
56
|
});
|
|
47
57
|
|
|
48
58
|
// Workspace members for bulk operations
|
|
49
|
-
const
|
|
59
|
+
const shouldLoadMembers =
|
|
60
|
+
canUseBoardAssignees !== false && isMultiSelectMode && selectedCount > 0;
|
|
61
|
+
const effectiveAssigneeMemberSource =
|
|
62
|
+
assigneeMemberSource ?? (workspace.personal ? 'board' : 'workspace');
|
|
63
|
+
const { data: workspaceMembersData = [] } = useWorkspaceMembers(
|
|
64
|
+
workspace.id,
|
|
65
|
+
{
|
|
66
|
+
enabled:
|
|
67
|
+
!!workspace.id &&
|
|
68
|
+
shouldLoadMembers &&
|
|
69
|
+
effectiveAssigneeMemberSource !== 'board',
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
const { data: boardViewableMembers = [] } = useQuery({
|
|
73
|
+
queryKey: ['task-board-viewable-members', workspace.id, boardId],
|
|
74
|
+
queryFn: async (): Promise<WorkspaceMember[]> => {
|
|
75
|
+
if (!workspace.id || !boardId) return [];
|
|
76
|
+
|
|
77
|
+
const payload = await listWorkspaceTaskBoardViewableMembers(
|
|
78
|
+
workspace.id,
|
|
79
|
+
boardId
|
|
80
|
+
);
|
|
81
|
+
const members = Array.isArray(payload?.members) ? payload.members : [];
|
|
82
|
+
|
|
83
|
+
return members.map((member) => ({
|
|
84
|
+
id: member.user_id,
|
|
85
|
+
user_id: member.user_id,
|
|
86
|
+
workspace_id: workspace.id,
|
|
87
|
+
display_name: member.display_name ?? member.email ?? member.user_id,
|
|
88
|
+
email: member.email ?? undefined,
|
|
89
|
+
avatar_url: member.avatar_url ?? undefined,
|
|
90
|
+
}));
|
|
91
|
+
},
|
|
50
92
|
enabled:
|
|
51
93
|
!!workspace.id &&
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
!!boardId &&
|
|
95
|
+
shouldLoadMembers &&
|
|
96
|
+
effectiveAssigneeMemberSource !== 'workspace',
|
|
97
|
+
staleTime: 5 * 60 * 1000,
|
|
55
98
|
});
|
|
99
|
+
const workspaceMembers: WorkspaceMember[] = [
|
|
100
|
+
...workspaceMembersData,
|
|
101
|
+
...boardViewableMembers.filter(
|
|
102
|
+
(boardMember) =>
|
|
103
|
+
!workspaceMembersData.some(
|
|
104
|
+
(workspaceMember) =>
|
|
105
|
+
(workspaceMember.user_id ?? workspaceMember.id) ===
|
|
106
|
+
boardMember.user_id
|
|
107
|
+
)
|
|
108
|
+
),
|
|
109
|
+
];
|
|
56
110
|
|
|
57
111
|
return { workspaceLabels, workspaceProjects, workspaceMembers };
|
|
58
112
|
}
|