@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
|
@@ -382,6 +382,44 @@ describe('BoardHeader', () => {
|
|
|
382
382
|
).not.toBeInTheDocument();
|
|
383
383
|
});
|
|
384
384
|
|
|
385
|
+
it('shows presence avatars for workspace boards', () => {
|
|
386
|
+
renderBoardHeader();
|
|
387
|
+
|
|
388
|
+
expect(screen.getByTestId('board-user-presence')).toBeInTheDocument();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('hides presence avatars for unshared personal boards', () => {
|
|
392
|
+
renderBoardHeader({
|
|
393
|
+
isPersonalWorkspace: true,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
expect(screen.queryByTestId('board-user-presence')).not.toBeInTheDocument();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('shows presence avatars for personal boards shared with guests', () => {
|
|
400
|
+
renderBoardHeader({
|
|
401
|
+
board: {
|
|
402
|
+
...mockBoard,
|
|
403
|
+
has_guest_access: true,
|
|
404
|
+
},
|
|
405
|
+
isPersonalWorkspace: true,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(screen.getByTestId('board-user-presence')).toBeInTheDocument();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('shows presence avatars for direct board guest access', () => {
|
|
412
|
+
renderBoardHeader({
|
|
413
|
+
board: {
|
|
414
|
+
...mockBoard,
|
|
415
|
+
access_type: 'guest',
|
|
416
|
+
},
|
|
417
|
+
isPersonalWorkspace: true,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
expect(screen.getByTestId('board-user-presence')).toBeInTheDocument();
|
|
421
|
+
});
|
|
422
|
+
|
|
385
423
|
it('updates status, view, and sort through combobox controls', () => {
|
|
386
424
|
const onFiltersChange = vi.fn();
|
|
387
425
|
const onListStatusFilterChange = vi.fn();
|
|
@@ -7,7 +7,7 @@ import { BoardSwitcher } from '../board-switcher';
|
|
|
7
7
|
const {
|
|
8
8
|
createWorkspaceTaskBoardMock,
|
|
9
9
|
isTaskRememberLastBoardEnabledMock,
|
|
10
|
-
|
|
10
|
+
listCurrentUserTaskBoardsMock,
|
|
11
11
|
pushMock,
|
|
12
12
|
rememberLastBoardConfig,
|
|
13
13
|
updateUserWorkspaceConfigMock,
|
|
@@ -17,7 +17,7 @@ const {
|
|
|
17
17
|
isTaskRememberLastBoardEnabledMock: vi.fn(
|
|
18
18
|
(value: string | null | undefined) => value !== 'false'
|
|
19
19
|
),
|
|
20
|
-
|
|
20
|
+
listCurrentUserTaskBoardsMock: vi.fn(),
|
|
21
21
|
pushMock: vi.fn(),
|
|
22
22
|
rememberLastBoardConfig: {
|
|
23
23
|
value: 'true' as string | null | undefined,
|
|
@@ -32,7 +32,14 @@ let comboboxProps:
|
|
|
32
32
|
creatingText?: string;
|
|
33
33
|
onChange: (value: string) => void;
|
|
34
34
|
onCreate?: (value: string) => Promise<{ label: string; value: string }>;
|
|
35
|
-
options: Array<{
|
|
35
|
+
options: Array<{
|
|
36
|
+
badge?: unknown;
|
|
37
|
+
description?: string;
|
|
38
|
+
group?: string;
|
|
39
|
+
icon?: unknown;
|
|
40
|
+
label: string;
|
|
41
|
+
value: string;
|
|
42
|
+
}>;
|
|
36
43
|
searchPlaceholder?: string;
|
|
37
44
|
selected?: string;
|
|
38
45
|
showSelectedIcon?: boolean;
|
|
@@ -43,9 +50,9 @@ vi.mock('@tuturuuu/internal-api/tasks', () => ({
|
|
|
43
50
|
createWorkspaceTaskBoard: (
|
|
44
51
|
...args: Parameters<typeof createWorkspaceTaskBoardMock>
|
|
45
52
|
) => createWorkspaceTaskBoardMock(...args),
|
|
46
|
-
|
|
47
|
-
...args: Parameters<typeof
|
|
48
|
-
) =>
|
|
53
|
+
listCurrentUserTaskBoards: (
|
|
54
|
+
...args: Parameters<typeof listCurrentUserTaskBoardsMock>
|
|
55
|
+
) => listCurrentUserTaskBoardsMock(...args),
|
|
49
56
|
}));
|
|
50
57
|
|
|
51
58
|
vi.mock('@tuturuuu/internal-api/users', () => ({
|
|
@@ -150,15 +157,25 @@ describe('BoardSwitcher', () => {
|
|
|
150
157
|
vi.clearAllMocks();
|
|
151
158
|
comboboxProps = undefined;
|
|
152
159
|
rememberLastBoardConfig.value = 'true';
|
|
153
|
-
|
|
160
|
+
listCurrentUserTaskBoardsMock.mockResolvedValue({
|
|
154
161
|
boards: [
|
|
155
162
|
{
|
|
163
|
+
access_type: 'member',
|
|
156
164
|
archived_at: null,
|
|
157
165
|
created_at: '2026-06-01T00:00:00.000Z',
|
|
158
166
|
deleted_at: null,
|
|
159
167
|
icon: null,
|
|
160
168
|
id: 'board-2',
|
|
161
169
|
name: 'Roadmap',
|
|
170
|
+
ticket_prefix: null,
|
|
171
|
+
workspace: {
|
|
172
|
+
avatar_url: null,
|
|
173
|
+
id: 'ws-1',
|
|
174
|
+
logo_url: null,
|
|
175
|
+
name: 'Current Workspace',
|
|
176
|
+
personal: false,
|
|
177
|
+
},
|
|
178
|
+
ws_id: 'ws-1',
|
|
162
179
|
},
|
|
163
180
|
],
|
|
164
181
|
});
|
|
@@ -188,6 +205,12 @@ describe('BoardSwitcher', () => {
|
|
|
188
205
|
});
|
|
189
206
|
expect(comboboxProps?.showSelectedIcon).toBeUndefined();
|
|
190
207
|
expect(comboboxProps?.options.some((option) => option.icon)).toBe(true);
|
|
208
|
+
const activeBoardOption = comboboxProps?.options.find(
|
|
209
|
+
(option) => option.value === 'board-2'
|
|
210
|
+
);
|
|
211
|
+
expect(activeBoardOption?.badge).toBeUndefined();
|
|
212
|
+
expect(activeBoardOption?.description).toBeUndefined();
|
|
213
|
+
expect(activeBoardOption?.group).toBe('Current Workspace');
|
|
191
214
|
|
|
192
215
|
fireEvent.click(screen.getByTestId('board-combobox'));
|
|
193
216
|
|
|
@@ -205,6 +228,69 @@ describe('BoardSwitcher', () => {
|
|
|
205
228
|
});
|
|
206
229
|
});
|
|
207
230
|
|
|
231
|
+
it('orders current workspace boards first and switches across workspaces without updating defaults', async () => {
|
|
232
|
+
listCurrentUserTaskBoardsMock.mockResolvedValue({
|
|
233
|
+
boards: [
|
|
234
|
+
{
|
|
235
|
+
access_type: 'member',
|
|
236
|
+
archived_at: null,
|
|
237
|
+
created_at: '2026-06-01T00:00:00.000Z',
|
|
238
|
+
deleted_at: null,
|
|
239
|
+
icon: null,
|
|
240
|
+
id: 'board-2',
|
|
241
|
+
name: 'External Roadmap',
|
|
242
|
+
ticket_prefix: null,
|
|
243
|
+
workspace: {
|
|
244
|
+
avatar_url: null,
|
|
245
|
+
id: 'ws-2',
|
|
246
|
+
logo_url: null,
|
|
247
|
+
name: 'Other Workspace',
|
|
248
|
+
personal: false,
|
|
249
|
+
},
|
|
250
|
+
ws_id: 'ws-2',
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
access_type: 'member',
|
|
254
|
+
archived_at: null,
|
|
255
|
+
created_at: '2026-06-01T00:00:00.000Z',
|
|
256
|
+
deleted_at: null,
|
|
257
|
+
icon: null,
|
|
258
|
+
id: 'board-current-secondary',
|
|
259
|
+
name: 'Current Roadmap',
|
|
260
|
+
ticket_prefix: null,
|
|
261
|
+
workspace: {
|
|
262
|
+
avatar_url: null,
|
|
263
|
+
id: 'ws-1',
|
|
264
|
+
logo_url: null,
|
|
265
|
+
name: 'Current Workspace',
|
|
266
|
+
personal: false,
|
|
267
|
+
},
|
|
268
|
+
ws_id: 'ws-1',
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
renderBoardSwitcher();
|
|
274
|
+
|
|
275
|
+
await waitFor(() => {
|
|
276
|
+
expect(comboboxProps?.options.map((option) => option.value)).toEqual([
|
|
277
|
+
'board-1',
|
|
278
|
+
'board-current-secondary',
|
|
279
|
+
'board-2',
|
|
280
|
+
]);
|
|
281
|
+
});
|
|
282
|
+
expect(comboboxProps?.options[2]).toMatchObject({
|
|
283
|
+
group: 'Other Workspace',
|
|
284
|
+
label: 'External Roadmap',
|
|
285
|
+
value: 'board-2',
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
fireEvent.click(screen.getByTestId('board-combobox'));
|
|
289
|
+
|
|
290
|
+
expect(pushMock).toHaveBeenCalledWith('/ws-2/tasks/boards/board-2');
|
|
291
|
+
expect(updateUserWorkspaceConfigMock).not.toHaveBeenCalled();
|
|
292
|
+
});
|
|
293
|
+
|
|
208
294
|
it('navigates without updating the default board when board memory is disabled', async () => {
|
|
209
295
|
rememberLastBoardConfig.value = 'false';
|
|
210
296
|
renderBoardSwitcher();
|
|
@@ -219,6 +305,41 @@ describe('BoardSwitcher', () => {
|
|
|
219
305
|
expect(updateUserWorkspaceConfigMock).not.toHaveBeenCalled();
|
|
220
306
|
});
|
|
221
307
|
|
|
308
|
+
it('hides board creation for guest-only board access', async () => {
|
|
309
|
+
listCurrentUserTaskBoardsMock.mockResolvedValue({
|
|
310
|
+
boards: [
|
|
311
|
+
{
|
|
312
|
+
access_type: 'guest',
|
|
313
|
+
archived_at: null,
|
|
314
|
+
created_at: '2026-06-01T00:00:00.000Z',
|
|
315
|
+
deleted_at: null,
|
|
316
|
+
guest_permission: 'edit',
|
|
317
|
+
icon: null,
|
|
318
|
+
id: 'board-1',
|
|
319
|
+
name: 'Tasks',
|
|
320
|
+
ticket_prefix: null,
|
|
321
|
+
workspace: {
|
|
322
|
+
avatar_url: null,
|
|
323
|
+
guest_products: ['tasks'],
|
|
324
|
+
id: 'ws-1',
|
|
325
|
+
logo_url: null,
|
|
326
|
+
name: 'Shared Workspace',
|
|
327
|
+
personal: false,
|
|
328
|
+
},
|
|
329
|
+
ws_id: 'ws-1',
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
renderBoardSwitcher();
|
|
335
|
+
|
|
336
|
+
await waitFor(() => {
|
|
337
|
+
expect(comboboxProps?.createText).toBeUndefined();
|
|
338
|
+
expect(comboboxProps?.creatingText).toBeUndefined();
|
|
339
|
+
expect(comboboxProps?.onCreate).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
222
343
|
it('creates a new board from the picker and opens it', async () => {
|
|
223
344
|
createWorkspaceTaskBoardMock.mockResolvedValue({
|
|
224
345
|
board: {
|
|
@@ -33,6 +33,11 @@ let kanbanBoardProps:
|
|
|
33
33
|
let listViewProps:
|
|
34
34
|
| React.ComponentProps<typeof import('../list-view')['ListView']>
|
|
35
35
|
| undefined;
|
|
36
|
+
let timelineBoardProps:
|
|
37
|
+
| React.ComponentProps<
|
|
38
|
+
typeof import('../../boards/boardId/timeline-board')['TimelineBoard']
|
|
39
|
+
>
|
|
40
|
+
| undefined;
|
|
36
41
|
|
|
37
42
|
vi.mock('next-intl', () => ({
|
|
38
43
|
useTranslations: () => (key: string) => key,
|
|
@@ -129,7 +134,10 @@ vi.mock('../list-view', () => ({
|
|
|
129
134
|
}));
|
|
130
135
|
|
|
131
136
|
vi.mock('../../boards/boardId/timeline-board', () => ({
|
|
132
|
-
TimelineBoard: () =>
|
|
137
|
+
TimelineBoard: (props: any) => {
|
|
138
|
+
timelineBoardProps = props;
|
|
139
|
+
return <div data-testid="timeline-view">Timeline</div>;
|
|
140
|
+
},
|
|
133
141
|
}));
|
|
134
142
|
|
|
135
143
|
const mockBoard = {
|
|
@@ -246,6 +254,7 @@ describe('BoardViews', () => {
|
|
|
246
254
|
boardHeaderProps = undefined;
|
|
247
255
|
kanbanBoardProps = undefined;
|
|
248
256
|
listViewProps = undefined;
|
|
257
|
+
timelineBoardProps = undefined;
|
|
249
258
|
createTaskMock.mockReset();
|
|
250
259
|
loadListPageMock.mockReset();
|
|
251
260
|
progressivePagination = {};
|
|
@@ -305,6 +314,61 @@ describe('BoardViews', () => {
|
|
|
305
314
|
expect(listWorkspaceTasksMock).not.toHaveBeenCalled();
|
|
306
315
|
});
|
|
307
316
|
|
|
317
|
+
it('enables assignees for personal boards that have guest access', async () => {
|
|
318
|
+
renderBoardViews({
|
|
319
|
+
board: {
|
|
320
|
+
...mockBoard,
|
|
321
|
+
has_guest_access: true,
|
|
322
|
+
},
|
|
323
|
+
workspace: { id: 'ws-1', personal: true },
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(kanbanBoardProps?.canUseBoardAssignees).toBe(true);
|
|
327
|
+
expect(kanbanBoardProps?.assigneeMemberSource).toBe('board');
|
|
328
|
+
|
|
329
|
+
await act(async () => {
|
|
330
|
+
boardHeaderProps?.onViewChange('list');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await waitFor(() => {
|
|
334
|
+
expect(screen.getByTestId('list-view')).toBeInTheDocument();
|
|
335
|
+
});
|
|
336
|
+
expect(listViewProps?.canUseBoardAssignees).toBe(true);
|
|
337
|
+
expect(listViewProps?.assigneeMemberSource).toBe('board');
|
|
338
|
+
|
|
339
|
+
await act(async () => {
|
|
340
|
+
boardHeaderProps?.onViewChange('timeline');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
await waitFor(() => {
|
|
344
|
+
expect(screen.getByTestId('timeline-view')).toBeInTheDocument();
|
|
345
|
+
});
|
|
346
|
+
expect(timelineBoardProps?.canUseBoardAssignees).toBe(true);
|
|
347
|
+
expect(timelineBoardProps?.assigneeMemberSource).toBe('board');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('keeps assignees hidden for unshared personal boards', () => {
|
|
351
|
+
renderBoardViews({
|
|
352
|
+
workspace: { id: 'ws-1', personal: true },
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(kanbanBoardProps?.canUseBoardAssignees).toBe(false);
|
|
356
|
+
expect(kanbanBoardProps?.assigneeMemberSource).toBe('workspace');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('merges workspace and board assignee sources for team boards that have guest access', () => {
|
|
360
|
+
renderBoardViews({
|
|
361
|
+
board: {
|
|
362
|
+
...mockBoard,
|
|
363
|
+
has_guest_access: true,
|
|
364
|
+
},
|
|
365
|
+
workspace: { id: 'ws-1', personal: false },
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(kanbanBoardProps?.canUseBoardAssignees).toBe(true);
|
|
369
|
+
expect(kanbanBoardProps?.assigneeMemberSource).toBe('workspace-and-board');
|
|
370
|
+
});
|
|
371
|
+
|
|
308
372
|
it('renders board-scoped drafts and recycle bin views from the header mode switcher', async () => {
|
|
309
373
|
renderBoardViews();
|
|
310
374
|
|
|
@@ -526,7 +590,7 @@ describe('BoardViews', () => {
|
|
|
526
590
|
);
|
|
527
591
|
});
|
|
528
592
|
|
|
529
|
-
it('
|
|
593
|
+
it('collapses the virtual external task list by default even when assigned external tasks exist', () => {
|
|
530
594
|
renderBoardViews({
|
|
531
595
|
workspace: {
|
|
532
596
|
...mockWorkspace,
|
|
@@ -549,7 +613,7 @@ describe('BoardViews', () => {
|
|
|
549
613
|
expect(kanbanBoardProps?.lists[0]).toEqual(
|
|
550
614
|
expect.objectContaining({
|
|
551
615
|
id: 'personal-external-staging:board-1',
|
|
552
|
-
is_external_collapsed:
|
|
616
|
+
is_external_collapsed: true,
|
|
553
617
|
is_external_staging: true,
|
|
554
618
|
})
|
|
555
619
|
);
|
|
@@ -648,10 +712,10 @@ describe('BoardViews', () => {
|
|
|
648
712
|
});
|
|
649
713
|
});
|
|
650
714
|
|
|
651
|
-
it('
|
|
715
|
+
it('collapses deadline sections by default and persists per board and section', async () => {
|
|
652
716
|
window.localStorage.setItem(
|
|
653
717
|
'task-board-deadline-section-collapsed:board-1:overdue',
|
|
654
|
-
'
|
|
718
|
+
'false'
|
|
655
719
|
);
|
|
656
720
|
|
|
657
721
|
renderBoardViews();
|
|
@@ -659,14 +723,14 @@ describe('BoardViews', () => {
|
|
|
659
723
|
await waitFor(() => {
|
|
660
724
|
expect(kanbanBoardProps?.deadlineSectionsCollapsed).toEqual(
|
|
661
725
|
expect.objectContaining({
|
|
662
|
-
overdue:
|
|
663
|
-
upcoming:
|
|
726
|
+
overdue: false,
|
|
727
|
+
upcoming: true,
|
|
664
728
|
})
|
|
665
729
|
);
|
|
666
730
|
});
|
|
667
731
|
|
|
668
732
|
act(() => {
|
|
669
|
-
kanbanBoardProps?.onDeadlineSectionCollapsedChange?.('upcoming',
|
|
733
|
+
kanbanBoardProps?.onDeadlineSectionCollapsedChange?.('upcoming', false);
|
|
670
734
|
});
|
|
671
735
|
|
|
672
736
|
await waitFor(() => {
|
|
@@ -674,13 +738,96 @@ describe('BoardViews', () => {
|
|
|
674
738
|
window.localStorage.getItem(
|
|
675
739
|
'task-board-deadline-section-collapsed:board-1:upcoming'
|
|
676
740
|
)
|
|
677
|
-
).toBe('
|
|
741
|
+
).toBe('false');
|
|
678
742
|
expect(kanbanBoardProps?.deadlineSectionsCollapsed).toEqual(
|
|
679
743
|
expect.objectContaining({
|
|
744
|
+
overdue: false,
|
|
745
|
+
upcoming: false,
|
|
746
|
+
})
|
|
747
|
+
);
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('does not force pinned special task lists to stay expanded', async () => {
|
|
752
|
+
getUserWorkspaceConfigMock.mockImplementation(
|
|
753
|
+
(_workspaceId: string, configId: string) =>
|
|
754
|
+
Promise.resolve({
|
|
755
|
+
value:
|
|
756
|
+
configId === 'TASK_BOARD_PINNED_SPECIAL_LISTS'
|
|
757
|
+
? JSON.stringify([
|
|
758
|
+
'external_tasks',
|
|
759
|
+
'closed_tasks',
|
|
760
|
+
'overdue',
|
|
761
|
+
'upcoming',
|
|
762
|
+
])
|
|
763
|
+
: null,
|
|
764
|
+
})
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
renderBoardViews({
|
|
768
|
+
lists: [...mockLists, closedList],
|
|
769
|
+
workspace: {
|
|
770
|
+
...mockWorkspace,
|
|
771
|
+
personal: true,
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
await waitFor(() => {
|
|
776
|
+
expect(kanbanBoardProps?.specialTaskListPins).toEqual(
|
|
777
|
+
expect.objectContaining({
|
|
778
|
+
closed_tasks: true,
|
|
779
|
+
external_tasks: true,
|
|
680
780
|
overdue: true,
|
|
681
781
|
upcoming: true,
|
|
682
782
|
})
|
|
683
783
|
);
|
|
784
|
+
expect(kanbanBoardProps?.lists[0]).toEqual(
|
|
785
|
+
expect.objectContaining({
|
|
786
|
+
is_external_collapsed: true,
|
|
787
|
+
is_external_staging: true,
|
|
788
|
+
})
|
|
789
|
+
);
|
|
790
|
+
expect(
|
|
791
|
+
kanbanBoardProps?.lists.find((list) => list.id === 'list-closed')
|
|
792
|
+
).toEqual(
|
|
793
|
+
expect.objectContaining({
|
|
794
|
+
is_collapsed: true,
|
|
795
|
+
status: 'closed',
|
|
796
|
+
})
|
|
797
|
+
);
|
|
798
|
+
expect(kanbanBoardProps?.deadlineSectionsCollapsed).toEqual(
|
|
799
|
+
expect.objectContaining({
|
|
800
|
+
overdue: true,
|
|
801
|
+
upcoming: true,
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
act(() => {
|
|
807
|
+
kanbanBoardProps?.onExternalTasksCollapsedChange?.(false);
|
|
808
|
+
kanbanBoardProps?.onTaskListCollapsedChange?.('list-closed', false);
|
|
809
|
+
kanbanBoardProps?.onDeadlineSectionCollapsedChange?.('overdue', false);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
await waitFor(() => {
|
|
813
|
+
expect(kanbanBoardProps?.lists[0]).toEqual(
|
|
814
|
+
expect.objectContaining({
|
|
815
|
+
is_external_collapsed: false,
|
|
816
|
+
})
|
|
817
|
+
);
|
|
818
|
+
expect(
|
|
819
|
+
kanbanBoardProps?.lists.find((list) => list.id === 'list-closed')
|
|
820
|
+
).toEqual(
|
|
821
|
+
expect.objectContaining({
|
|
822
|
+
is_collapsed: false,
|
|
823
|
+
})
|
|
824
|
+
);
|
|
825
|
+
expect(kanbanBoardProps?.deadlineSectionsCollapsed).toEqual(
|
|
826
|
+
expect.objectContaining({
|
|
827
|
+
overdue: false,
|
|
828
|
+
upcoming: true,
|
|
829
|
+
})
|
|
830
|
+
);
|
|
684
831
|
});
|
|
685
832
|
});
|
|
686
833
|
|
|
@@ -987,6 +1134,72 @@ describe('BoardViews', () => {
|
|
|
987
1134
|
});
|
|
988
1135
|
});
|
|
989
1136
|
|
|
1137
|
+
it('uses the route default view instead of stale local My Tasks config', async () => {
|
|
1138
|
+
window.localStorage.setItem(
|
|
1139
|
+
getBoardConfigKey(mockBoard.id),
|
|
1140
|
+
JSON.stringify({
|
|
1141
|
+
currentView: 'my_tasks',
|
|
1142
|
+
filters: {
|
|
1143
|
+
assignees: [],
|
|
1144
|
+
dueDateRange: null,
|
|
1145
|
+
estimationRange: null,
|
|
1146
|
+
includeMyTasks: false,
|
|
1147
|
+
includeUnassigned: false,
|
|
1148
|
+
labels: [],
|
|
1149
|
+
priorities: [],
|
|
1150
|
+
projects: [],
|
|
1151
|
+
sourceBoardIds: [],
|
|
1152
|
+
sourceScope: 'all_visible',
|
|
1153
|
+
sourceWorkspaceIds: [],
|
|
1154
|
+
},
|
|
1155
|
+
listStatusFilter: 'all',
|
|
1156
|
+
})
|
|
1157
|
+
);
|
|
1158
|
+
window.history.replaceState({}, '', '/ws-1/tasks/boards/board-1');
|
|
1159
|
+
|
|
1160
|
+
renderBoardViews({ props: { defaultView: 'kanban' } });
|
|
1161
|
+
|
|
1162
|
+
await waitFor(() => {
|
|
1163
|
+
expect(screen.getByTestId('kanban-view')).toBeInTheDocument();
|
|
1164
|
+
});
|
|
1165
|
+
expect(screen.queryByTestId('my-tasks-view')).not.toBeInTheDocument();
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it('lets an explicit URL view override the route default view', async () => {
|
|
1169
|
+
window.localStorage.setItem(
|
|
1170
|
+
getBoardConfigKey(mockBoard.id),
|
|
1171
|
+
JSON.stringify({
|
|
1172
|
+
currentView: 'timeline',
|
|
1173
|
+
filters: {
|
|
1174
|
+
assignees: [],
|
|
1175
|
+
dueDateRange: null,
|
|
1176
|
+
estimationRange: null,
|
|
1177
|
+
includeMyTasks: false,
|
|
1178
|
+
includeUnassigned: false,
|
|
1179
|
+
labels: [],
|
|
1180
|
+
priorities: [],
|
|
1181
|
+
projects: [],
|
|
1182
|
+
sourceBoardIds: [],
|
|
1183
|
+
sourceScope: 'all_visible',
|
|
1184
|
+
sourceWorkspaceIds: [],
|
|
1185
|
+
},
|
|
1186
|
+
listStatusFilter: 'all',
|
|
1187
|
+
})
|
|
1188
|
+
);
|
|
1189
|
+
window.history.replaceState(
|
|
1190
|
+
{},
|
|
1191
|
+
'',
|
|
1192
|
+
'/ws-1/tasks/boards/board-1?view=my_tasks'
|
|
1193
|
+
);
|
|
1194
|
+
|
|
1195
|
+
renderBoardViews({ props: { defaultView: 'kanban' } });
|
|
1196
|
+
|
|
1197
|
+
await waitFor(() => {
|
|
1198
|
+
expect(screen.getByTestId('my-tasks-view')).toBeInTheDocument();
|
|
1199
|
+
});
|
|
1200
|
+
expect(screen.queryByTestId('kanban-view')).not.toBeInTheDocument();
|
|
1201
|
+
});
|
|
1202
|
+
|
|
990
1203
|
it('ignores board hotkeys while typing in an input', async () => {
|
|
991
1204
|
renderBoardViews();
|
|
992
1205
|
const input = screen.getByTestId('board-header-input');
|
|
@@ -10,8 +10,29 @@ describe('TaskBoardLoadingState', () => {
|
|
|
10
10
|
expect(screen.getByTestId('task-board-loading-state')).toHaveClass(
|
|
11
11
|
'-m-4',
|
|
12
12
|
'h-[calc(100dvh+2rem)]',
|
|
13
|
+
'w-[calc(100%+2rem)]',
|
|
13
14
|
'bg-transparent'
|
|
14
15
|
);
|
|
15
16
|
expect(screen.getByTestId('kanban-skeleton')).toHaveClass('bg-transparent');
|
|
17
|
+
expect(screen.getByTestId('kanban-skeleton-frame')).toHaveClass(
|
|
18
|
+
'py-2',
|
|
19
|
+
'pl-2',
|
|
20
|
+
'pr-0'
|
|
21
|
+
);
|
|
22
|
+
expect(screen.getByTestId('kanban-skeleton-frame')).not.toHaveClass('p-2');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('keeps embedded loading skeletons constrained to the parent width', () => {
|
|
26
|
+
render(<TaskBoardLoadingState />);
|
|
27
|
+
|
|
28
|
+
expect(screen.getByTestId('task-board-loading-state')).toHaveClass(
|
|
29
|
+
'w-full',
|
|
30
|
+
'h-[calc(100dvh-1rem)]'
|
|
31
|
+
);
|
|
32
|
+
expect(screen.getByTestId('task-board-loading-state')).not.toHaveClass(
|
|
33
|
+
'-m-4',
|
|
34
|
+
'w-[calc(100%+2rem)]'
|
|
35
|
+
);
|
|
36
|
+
expect(screen.getByTestId('kanban-skeleton-frame')).toHaveClass('p-2');
|
|
16
37
|
});
|
|
17
38
|
});
|