@tuturuuu/ui 0.13.0 → 0.16.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 +90 -0
- package/biome.json +1 -1
- package/package.json +62 -62
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +12 -0
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +93 -6
- package/src/components/ui/custom/loading-statistic-card.tsx +18 -0
- package/src/components/ui/custom/notification-popover-client.tsx +5 -1
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.test.tsx +34 -0
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +106 -0
- package/src/components/ui/custom/settings/sidebar-settings.tsx +18 -6
- package/src/components/ui/custom/settings/task-settings.tsx +55 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +18 -12
- package/src/components/ui/custom/sidebar-context.tsx +58 -3
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +5 -3
- package/src/components/ui/custom/statistic-card.tsx +104 -0
- package/src/components/ui/custom/structure.test.tsx +29 -0
- package/src/components/ui/custom/structure.tsx +103 -88
- package/src/components/ui/finance/invoices/civil-month.ts +224 -0
- package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.test.tsx +49 -0
- package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.tsx +81 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +90 -2
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +40 -4
- package/src/components/ui/finance/invoices/pending-columns.test.ts +26 -0
- package/src/components/ui/finance/invoices/pending-columns.tsx +45 -14
- package/src/components/ui/finance/invoices/standard-invoice.test.ts +31 -0
- package/src/components/ui/finance/invoices/standard-invoice.tsx +33 -16
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +331 -0
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +53 -28
- package/src/components/ui/finance/invoices/utils.test.ts +84 -0
- package/src/components/ui/finance/invoices/utils.ts +56 -134
- package/src/components/ui/resizable.test.ts +15 -0
- package/src/components/ui/resizable.tsx +28 -6
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +4 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +62 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.test.ts +118 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.ts +144 -70
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +48 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +50 -6
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +6 -0
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +4 -2
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +41 -3
- package/src/components/ui/tu-do/boards/boardId/menus/task-due-date-menu.tsx +31 -10
- package/src/components/ui/tu-do/boards/boardId/menus/task-priority-menu.tsx +7 -5
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +7 -5
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.test.ts +44 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +44 -30
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +54 -0
- package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +43 -1
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +33 -15
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +3 -3
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +74 -28
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +12 -10
- package/src/components/ui/tu-do/projects/components/project-grid-card.tsx +22 -6
- package/src/components/ui/tu-do/projects/components/project-list-item.tsx +24 -8
- package/src/components/ui/tu-do/projects/projectId/components/__tests__/tasks-tab-layout.test.tsx +6 -0
- package/src/components/ui/tu-do/projects/projectId/components/tasks-tab.tsx +3 -1
- package/src/components/ui/tu-do/projects/projectId/task-project-detail-page-client.tsx +11 -3
- package/src/components/ui/tu-do/projects/projectId/task-project-detail.tsx +2 -0
- package/src/components/ui/tu-do/projects/projectId/types.ts +1 -0
- package/src/components/ui/tu-do/projects/task-projects-client.tsx +45 -1
- package/src/components/ui/tu-do/projects/types.ts +4 -1
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +10 -0
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +87 -2
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +70 -0
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +27 -3
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +41 -3
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +54 -0
- package/src/components/ui/tu-do/shared/board-header.tsx +20 -0
- package/src/components/ui/tu-do/shared/board-views.tsx +33 -3
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +2 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +7 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-mutations.test.tsx +231 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +139 -68
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.test.tsx +68 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +17 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +45 -23
- package/src/components/ui/tu-do/shared/task-quick-create-target-list.ts +20 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.test.tsx +138 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +15 -9
- package/src/components/ui/tu-do/templates/client.tsx +97 -78
- package/src/components/ui/tu-do/templates/marketplace/client.tsx +171 -135
- package/src/components/ui/tu-do/templates/task-templates-hub.tsx +3 -0
- package/src/components/ui/tu-do/templates/templateId/client.tsx +24 -9
- package/src/components/ui/tu-do/templates/templateId/task-template-detail-page-client.tsx +8 -2
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +14 -39
- package/src/hooks/task-actions-personal-external.ts +12 -6
- package/src/hooks/use-notifications.ts +14 -2
- package/src/hooks/use-user-config.ts +10 -2
- package/vitest.config.ts +4 -0
|
@@ -17,7 +17,9 @@ import { getBoardConfigKey } from '../board-config-storage';
|
|
|
17
17
|
import { BoardViews } from '../board-views';
|
|
18
18
|
|
|
19
19
|
const listWorkspaceTasksMock = vi.hoisted(() => vi.fn());
|
|
20
|
+
const getUserConfigMock = vi.hoisted(() => vi.fn());
|
|
20
21
|
const getUserWorkspaceConfigMock = vi.hoisted(() => vi.fn());
|
|
22
|
+
const updateUserConfigMock = vi.hoisted(() => vi.fn());
|
|
21
23
|
const updateUserWorkspaceConfigMock = vi.hoisted(() => vi.fn());
|
|
22
24
|
const createTaskMock = vi.fn();
|
|
23
25
|
const loadListPageMock = vi.fn();
|
|
@@ -64,8 +66,11 @@ vi.mock('@tuturuuu/internal-api/tasks', () => ({
|
|
|
64
66
|
vi.mock('@tuturuuu/internal-api/users', () => ({
|
|
65
67
|
TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID: 'TASK_BOARD_PINNED_SPECIAL_LISTS',
|
|
66
68
|
TASK_LAST_BOARD_VIEW_CONFIG_ID: 'TASK_LAST_BOARD_VIEW',
|
|
69
|
+
TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID: 'TASK_QUICK_CREATE_TARGET_LIST',
|
|
70
|
+
getUserConfig: (...args: unknown[]) => getUserConfigMock(...args),
|
|
67
71
|
getUserWorkspaceConfig: (...args: unknown[]) =>
|
|
68
72
|
getUserWorkspaceConfigMock(...args),
|
|
73
|
+
updateUserConfig: (...args: unknown[]) => updateUserConfigMock(...args),
|
|
69
74
|
updateUserWorkspaceConfig: (...args: unknown[]) =>
|
|
70
75
|
updateUserWorkspaceConfigMock(...args),
|
|
71
76
|
}));
|
|
@@ -260,8 +265,12 @@ describe('BoardViews', () => {
|
|
|
260
265
|
progressivePagination = {};
|
|
261
266
|
listWorkspaceTasksMock.mockReset();
|
|
262
267
|
listWorkspaceTasksMock.mockResolvedValue({ tasks: mockTasks });
|
|
268
|
+
getUserConfigMock.mockReset();
|
|
269
|
+
getUserConfigMock.mockResolvedValue({ value: null });
|
|
263
270
|
getUserWorkspaceConfigMock.mockReset();
|
|
264
271
|
getUserWorkspaceConfigMock.mockResolvedValue({ value: null });
|
|
272
|
+
updateUserConfigMock.mockReset();
|
|
273
|
+
updateUserConfigMock.mockResolvedValue({ message: 'ok' });
|
|
265
274
|
updateUserWorkspaceConfigMock.mockReset();
|
|
266
275
|
updateUserWorkspaceConfigMock.mockResolvedValue({ message: 'ok' });
|
|
267
276
|
window.localStorage.clear();
|
|
@@ -312,6 +321,7 @@ describe('BoardViews', () => {
|
|
|
312
321
|
expect(boardHeaderProps?.titlePrefix).toBeDefined();
|
|
313
322
|
expect(kanbanBoardProps?.readOnly).toBe(true);
|
|
314
323
|
expect(listWorkspaceTasksMock).not.toHaveBeenCalled();
|
|
324
|
+
expect(getUserConfigMock).not.toHaveBeenCalled();
|
|
315
325
|
});
|
|
316
326
|
|
|
317
327
|
it('enables assignees for personal boards that have guest access', async () => {
|
|
@@ -544,6 +554,66 @@ describe('BoardViews', () => {
|
|
|
544
554
|
);
|
|
545
555
|
});
|
|
546
556
|
|
|
557
|
+
it('creates a task in the hovered list when the quick-create target prefers hovered lists', async () => {
|
|
558
|
+
getUserConfigMock.mockResolvedValue({ value: 'hovered_list' });
|
|
559
|
+
const { queryClient } = renderBoardViews({
|
|
560
|
+
board: { ...mockBoard, default_list_id: 'list-1' },
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await waitFor(() => {
|
|
564
|
+
expect(
|
|
565
|
+
queryClient.getQueryData([
|
|
566
|
+
'user-config',
|
|
567
|
+
'TASK_QUICK_CREATE_TARGET_LIST',
|
|
568
|
+
])
|
|
569
|
+
).toBe('hovered_list');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
await act(async () => {
|
|
573
|
+
kanbanBoardProps?.onHoveredTaskListChange?.('list-2');
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
fireEvent.keyDown(document, { key: 'c' });
|
|
577
|
+
|
|
578
|
+
expect(createTaskMock).toHaveBeenCalledTimes(1);
|
|
579
|
+
expect(createTaskMock).toHaveBeenCalledWith(
|
|
580
|
+
'board-1',
|
|
581
|
+
'list-2',
|
|
582
|
+
mockLists,
|
|
583
|
+
expect.objectContaining({ labels: [] })
|
|
584
|
+
);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('falls back to the board default list in hovered-list mode when no hovered list is selectable', async () => {
|
|
588
|
+
getUserConfigMock.mockResolvedValue({ value: 'hovered_list' });
|
|
589
|
+
const { queryClient } = renderBoardViews({
|
|
590
|
+
board: { ...mockBoard, default_list_id: 'list-2' },
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
await waitFor(() => {
|
|
594
|
+
expect(
|
|
595
|
+
queryClient.getQueryData([
|
|
596
|
+
'user-config',
|
|
597
|
+
'TASK_QUICK_CREATE_TARGET_LIST',
|
|
598
|
+
])
|
|
599
|
+
).toBe('hovered_list');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
await act(async () => {
|
|
603
|
+
kanbanBoardProps?.onHoveredTaskListChange?.('external-list');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
fireEvent.keyDown(document, { key: 'c' });
|
|
607
|
+
|
|
608
|
+
expect(createTaskMock).toHaveBeenCalledTimes(1);
|
|
609
|
+
expect(createTaskMock).toHaveBeenCalledWith(
|
|
610
|
+
'board-1',
|
|
611
|
+
'list-2',
|
|
612
|
+
mockLists,
|
|
613
|
+
expect.objectContaining({ labels: [] })
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
|
|
547
617
|
it('falls back to the first list when the default list is unavailable', () => {
|
|
548
618
|
renderBoardViews({
|
|
549
619
|
board: { ...mockBoard, default_list_id: 'list-does-not-exist' },
|
|
@@ -8,11 +8,17 @@ describe('TaskBoardLoadingState', () => {
|
|
|
8
8
|
render(<TaskBoardLoadingState root />);
|
|
9
9
|
|
|
10
10
|
expect(screen.getByTestId('task-board-loading-state')).toHaveClass(
|
|
11
|
-
'-
|
|
11
|
+
'-mt-4',
|
|
12
|
+
'-mb-4',
|
|
13
|
+
'-ml-4',
|
|
12
14
|
'h-[calc(100dvh+2rem)]',
|
|
13
15
|
'w-[calc(100%+2rem)]',
|
|
14
16
|
'bg-transparent'
|
|
15
17
|
);
|
|
18
|
+
expect(screen.getByTestId('task-board-loading-state')).not.toHaveClass(
|
|
19
|
+
'-m-4',
|
|
20
|
+
'-mr-4'
|
|
21
|
+
);
|
|
16
22
|
expect(screen.getByTestId('kanban-skeleton')).toHaveClass('bg-transparent');
|
|
17
23
|
expect(screen.getByTestId('kanban-skeleton-frame')).toHaveClass(
|
|
18
24
|
'py-2',
|
|
@@ -34,6 +40,9 @@ describe('TaskBoardLoadingState', () => {
|
|
|
34
40
|
);
|
|
35
41
|
expect(screen.getByTestId('task-board-loading-state')).not.toHaveClass(
|
|
36
42
|
'-m-4',
|
|
43
|
+
'-mt-4',
|
|
44
|
+
'-mb-4',
|
|
45
|
+
'-ml-4',
|
|
37
46
|
'w-[calc(100%+2rem)]'
|
|
38
47
|
);
|
|
39
48
|
expect(screen.getByTestId('kanban-skeleton-frame')).toHaveClass('p-2');
|
|
@@ -46,22 +55,37 @@ describe('TaskBoardLoadingState', () => {
|
|
|
46
55
|
render(<TaskBoardLoadingState root showHeader />);
|
|
47
56
|
|
|
48
57
|
expect(screen.getByTestId('task-board-loading-state')).toHaveClass(
|
|
49
|
-
'-
|
|
58
|
+
'-mt-4',
|
|
59
|
+
'-mb-4',
|
|
60
|
+
'-ml-4',
|
|
50
61
|
'h-[calc(100dvh+2rem)]',
|
|
51
62
|
'w-[calc(100%+2rem)]',
|
|
52
63
|
'flex',
|
|
53
64
|
'flex-col'
|
|
54
65
|
);
|
|
66
|
+
expect(screen.getByTestId('task-board-loading-state')).not.toHaveClass(
|
|
67
|
+
'-m-4',
|
|
68
|
+
'-mr-4'
|
|
69
|
+
);
|
|
55
70
|
expect(screen.getByTestId('task-board-header-skeleton')).toHaveAttribute(
|
|
56
71
|
'aria-hidden',
|
|
57
72
|
'true'
|
|
58
73
|
);
|
|
59
74
|
expect(screen.getByTestId('task-board-header-skeleton')).toHaveClass(
|
|
60
75
|
'border-b',
|
|
61
|
-
'
|
|
76
|
+
'pl-2',
|
|
77
|
+
'pr-0',
|
|
62
78
|
'pt-2',
|
|
63
79
|
'pb-2'
|
|
64
80
|
);
|
|
81
|
+
expect(screen.getByTestId('task-board-header-skeleton')).not.toHaveClass(
|
|
82
|
+
'px-2'
|
|
83
|
+
);
|
|
84
|
+
expect(screen.getByTestId('kanban-skeleton-frame')).toHaveClass(
|
|
85
|
+
'py-2',
|
|
86
|
+
'pl-2',
|
|
87
|
+
'pr-0'
|
|
88
|
+
);
|
|
65
89
|
expect(screen.getByTestId('task-board-header-skeleton')).not.toHaveClass(
|
|
66
90
|
'-mt-2'
|
|
67
91
|
);
|
|
@@ -171,6 +171,8 @@ vi.mock('../task-edit-dialog', () => ({
|
|
|
171
171
|
isHydratingTask,
|
|
172
172
|
taskLoadError,
|
|
173
173
|
task,
|
|
174
|
+
visibleBoardId,
|
|
175
|
+
visibleTaskSnapshot,
|
|
174
176
|
onClose,
|
|
175
177
|
onNavigateToTask,
|
|
176
178
|
}: {
|
|
@@ -179,6 +181,8 @@ vi.mock('../task-edit-dialog', () => ({
|
|
|
179
181
|
isHydratingTask?: boolean;
|
|
180
182
|
taskLoadError?: boolean;
|
|
181
183
|
task?: Task;
|
|
184
|
+
visibleBoardId?: string;
|
|
185
|
+
visibleTaskSnapshot?: Partial<Task>;
|
|
182
186
|
onClose: () => void;
|
|
183
187
|
onNavigateToTask?: (taskId: string) => Promise<void>;
|
|
184
188
|
}) => {
|
|
@@ -197,6 +201,8 @@ vi.mock('../task-edit-dialog', () => ({
|
|
|
197
201
|
data-hydrating={String(!!isHydratingTask)}
|
|
198
202
|
data-load-error={String(!!taskLoadError)}
|
|
199
203
|
data-open={isOpen}
|
|
204
|
+
data-visible-board-id={visibleBoardId}
|
|
205
|
+
data-visible-list-id={visibleTaskSnapshot?.list_id}
|
|
200
206
|
>
|
|
201
207
|
{task && <div data-testid="task-name">{task.name}</div>}
|
|
202
208
|
<button
|
|
@@ -684,11 +690,25 @@ describe('TaskDialogManager', () => {
|
|
|
684
690
|
const { openTaskById } = useTaskDialogContext();
|
|
685
691
|
|
|
686
692
|
React.useEffect(() => {
|
|
693
|
+
const visibleExternalSnapshot = {
|
|
694
|
+
...mockTask,
|
|
695
|
+
id: 'external-task-1',
|
|
696
|
+
name: 'External snapshot',
|
|
697
|
+
list_id: 'personal-list-1',
|
|
698
|
+
personal_board_id: 'personal-board-1',
|
|
699
|
+
personal_list_id: 'personal-list-1',
|
|
700
|
+
personal_sort_key: 2000,
|
|
701
|
+
source_workspace_id: 'source-workspace-1',
|
|
702
|
+
source_board_id: 'external-board-1',
|
|
703
|
+
source_list_id: 'external-list-1',
|
|
704
|
+
source_list_name: 'External list',
|
|
705
|
+
is_personal_external: true,
|
|
706
|
+
} satisfies Task;
|
|
707
|
+
|
|
687
708
|
void openTaskById('external-task-1', {
|
|
688
709
|
initialTask: {
|
|
689
|
-
...
|
|
690
|
-
|
|
691
|
-
name: 'External snapshot',
|
|
710
|
+
...visibleExternalSnapshot,
|
|
711
|
+
list_id: 'external-list-1',
|
|
692
712
|
},
|
|
693
713
|
boardId: 'external-board-1',
|
|
694
714
|
availableLists: [
|
|
@@ -700,6 +720,8 @@ describe('TaskDialogManager', () => {
|
|
|
700
720
|
],
|
|
701
721
|
taskWsId: 'source-workspace-1',
|
|
702
722
|
taskWorkspacePersonal: false,
|
|
723
|
+
visibleBoardId: 'personal-board-1',
|
|
724
|
+
visibleTaskSnapshot: visibleExternalSnapshot,
|
|
703
725
|
});
|
|
704
726
|
}, [openTaskById]);
|
|
705
727
|
|
|
@@ -719,6 +741,14 @@ describe('TaskDialogManager', () => {
|
|
|
719
741
|
);
|
|
720
742
|
expect(getByTestId('task-name')).toHaveTextContent('External snapshot');
|
|
721
743
|
});
|
|
744
|
+
expect(getByTestId('task-edit-dialog')).toHaveAttribute(
|
|
745
|
+
'data-visible-board-id',
|
|
746
|
+
'personal-board-1'
|
|
747
|
+
);
|
|
748
|
+
expect(getByTestId('task-edit-dialog')).toHaveAttribute(
|
|
749
|
+
'data-visible-list-id',
|
|
750
|
+
'personal-list-1'
|
|
751
|
+
);
|
|
722
752
|
expect(taskDialogRenderStats).toMatchObject({
|
|
723
753
|
mounts: 1,
|
|
724
754
|
unmounts: 0,
|
|
@@ -751,6 +781,14 @@ describe('TaskDialogManager', () => {
|
|
|
751
781
|
'Hydrated external task'
|
|
752
782
|
);
|
|
753
783
|
});
|
|
784
|
+
expect(getByTestId('task-edit-dialog')).toHaveAttribute(
|
|
785
|
+
'data-visible-board-id',
|
|
786
|
+
'personal-board-1'
|
|
787
|
+
);
|
|
788
|
+
expect(getByTestId('task-edit-dialog')).toHaveAttribute(
|
|
789
|
+
'data-visible-list-id',
|
|
790
|
+
'personal-list-1'
|
|
791
|
+
);
|
|
754
792
|
expect(taskDialogRenderStats).toMatchObject({
|
|
755
793
|
mounts: 1,
|
|
756
794
|
unmounts: 0,
|
|
@@ -515,6 +515,60 @@ describe('useProgressiveBoardLoader', () => {
|
|
|
515
515
|
]);
|
|
516
516
|
});
|
|
517
517
|
|
|
518
|
+
it('preserves a freshly property-updated external overlay when list revalidation misses it', async () => {
|
|
519
|
+
const propertyUpdatedExternalTask = {
|
|
520
|
+
id: 'task-external',
|
|
521
|
+
display_number: 43,
|
|
522
|
+
name: 'Updated external task',
|
|
523
|
+
list_id: 'personal-list-2',
|
|
524
|
+
created_at: '2026-05-07T00:00:00.000Z',
|
|
525
|
+
sort_key: 1_500_000,
|
|
526
|
+
personal_board_id: 'board-1',
|
|
527
|
+
personal_list_id: 'personal-list-2',
|
|
528
|
+
personal_sort_key: 1_500_000,
|
|
529
|
+
source_workspace_id: 'source-ws',
|
|
530
|
+
source_board_id: 'source-board',
|
|
531
|
+
source_list_id: 'source-list',
|
|
532
|
+
source_list_name: 'Source list',
|
|
533
|
+
source_list_status: 'active',
|
|
534
|
+
is_personal_external: true,
|
|
535
|
+
priority: 'high',
|
|
536
|
+
_localMutationAt: Date.now(),
|
|
537
|
+
} as Task;
|
|
538
|
+
|
|
539
|
+
const { result } = renderHook(
|
|
540
|
+
() => useProgressiveBoardLoader('personal', 'board-1'),
|
|
541
|
+
{ wrapper }
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
vi.mocked(listWorkspaceTasks).mockResolvedValueOnce({ tasks: [] });
|
|
545
|
+
|
|
546
|
+
await act(async () => {
|
|
547
|
+
await result.current.loadListPage('personal-list-2', 0);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
queryClient.setQueryData(
|
|
551
|
+
['tasks', 'board-1'],
|
|
552
|
+
[propertyUpdatedExternalTask]
|
|
553
|
+
);
|
|
554
|
+
vi.mocked(listWorkspaceTasks).mockResolvedValueOnce({ tasks: [] });
|
|
555
|
+
|
|
556
|
+
await act(async () => {
|
|
557
|
+
await result.current.revalidateLoadedLists();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
expect(queryClient.getQueryData<Task[]>(['tasks', 'board-1'])).toEqual([
|
|
561
|
+
expect.objectContaining({
|
|
562
|
+
id: 'task-external',
|
|
563
|
+
priority: 'high',
|
|
564
|
+
list_id: 'personal-list-2',
|
|
565
|
+
personal_list_id: 'personal-list-2',
|
|
566
|
+
source_list_id: 'source-list',
|
|
567
|
+
_localMutationAt: expect.any(Number),
|
|
568
|
+
}),
|
|
569
|
+
]);
|
|
570
|
+
});
|
|
571
|
+
|
|
518
572
|
it('revalidates loaded lists without clearing other list tasks', async () => {
|
|
519
573
|
const existingOtherListTask: Task = {
|
|
520
574
|
id: 'task-other',
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
LayoutGrid,
|
|
16
16
|
List,
|
|
17
17
|
Loader2,
|
|
18
|
+
PanelLeftClose,
|
|
18
19
|
Pencil,
|
|
19
20
|
Play,
|
|
20
21
|
Search,
|
|
@@ -29,6 +30,7 @@ import type { WorkspaceTaskBoard } from '@tuturuuu/types';
|
|
|
29
30
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
30
31
|
import { Button } from '@tuturuuu/ui/button';
|
|
31
32
|
import { Combobox } from '@tuturuuu/ui/custom/combobox';
|
|
33
|
+
import { useOptionalSidebar } from '@tuturuuu/ui/custom/sidebar-context';
|
|
32
34
|
import { Input } from '@tuturuuu/ui/input';
|
|
33
35
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
|
|
34
36
|
import { cn } from '@tuturuuu/utils/format';
|
|
@@ -157,6 +159,9 @@ export function BoardHeader({
|
|
|
157
159
|
!publicView &&
|
|
158
160
|
isPersonalWorkspace &&
|
|
159
161
|
currentView === 'kanban';
|
|
162
|
+
const sidebar = useOptionalSidebar();
|
|
163
|
+
const hideSidebarVisible =
|
|
164
|
+
Boolean(sidebar) && sidebar?.behavior !== 'hidden' && !publicView;
|
|
160
165
|
const hasSharedBoardGuests =
|
|
161
166
|
board.access_type === 'guest' || board.has_guest_access === true;
|
|
162
167
|
const presenceVisible =
|
|
@@ -568,6 +573,21 @@ export function BoardHeader({
|
|
|
568
573
|
/>
|
|
569
574
|
)}
|
|
570
575
|
|
|
576
|
+
{hideSidebarVisible && (
|
|
577
|
+
<ToolbarTooltip label={t('common.hide_sidebar')}>
|
|
578
|
+
<Button
|
|
579
|
+
type="button"
|
|
580
|
+
size="xs"
|
|
581
|
+
variant="outline"
|
|
582
|
+
className={toolbarButtonClass}
|
|
583
|
+
onClick={() => sidebar?.handleBehaviorChange('hidden')}
|
|
584
|
+
aria-label={t('common.hide_sidebar')}
|
|
585
|
+
>
|
|
586
|
+
<PanelLeftClose className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
|
|
587
|
+
</Button>
|
|
588
|
+
</ToolbarTooltip>
|
|
589
|
+
)}
|
|
590
|
+
|
|
571
591
|
{/* Smart Focus Button */}
|
|
572
592
|
{interactiveControlsVisible && (
|
|
573
593
|
<ToolbarTooltip
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
|
|
15
15
|
TASK_LAST_BOARD_VIEW_CONFIG_ID,
|
|
16
|
+
TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID,
|
|
16
17
|
} from '@tuturuuu/internal-api/users';
|
|
17
18
|
import type {
|
|
18
19
|
Workspace,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
useMemo,
|
|
36
37
|
useState,
|
|
37
38
|
} from 'react';
|
|
39
|
+
import { useUserConfig } from '../../../../hooks/use-user-config';
|
|
38
40
|
import {
|
|
39
41
|
useUpdateUserWorkspaceConfig,
|
|
40
42
|
useUserWorkspaceConfig,
|
|
@@ -58,6 +60,10 @@ import {
|
|
|
58
60
|
type SpecialTaskListPin,
|
|
59
61
|
serializeSpecialTaskListPins,
|
|
60
62
|
} from './special-task-list-pins';
|
|
63
|
+
import {
|
|
64
|
+
DEFAULT_TASK_QUICK_CREATE_TARGET_LIST,
|
|
65
|
+
normalizeTaskQuickCreateTargetList,
|
|
66
|
+
} from './task-quick-create-target-list';
|
|
61
67
|
|
|
62
68
|
export type ViewType =
|
|
63
69
|
| 'kanban'
|
|
@@ -290,8 +296,19 @@ export function BoardViews({
|
|
|
290
296
|
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false);
|
|
291
297
|
const [kanbanBulkSelectionActive, setKanbanBulkSelectionActive] =
|
|
292
298
|
useState(false);
|
|
299
|
+
const [hoveredTaskListId, setHoveredTaskListId] = useState<string | null>(
|
|
300
|
+
null
|
|
301
|
+
);
|
|
293
302
|
const { createTask } = useTaskDialog();
|
|
294
303
|
const localTaskState = readOnly || publicView;
|
|
304
|
+
const { data: quickCreateTargetListRaw } = useUserConfig(
|
|
305
|
+
TASK_QUICK_CREATE_TARGET_LIST_CONFIG_ID,
|
|
306
|
+
DEFAULT_TASK_QUICK_CREATE_TARGET_LIST,
|
|
307
|
+
{ enabled: !localTaskState }
|
|
308
|
+
);
|
|
309
|
+
const quickCreateTargetList = normalizeTaskQuickCreateTargetList(
|
|
310
|
+
quickCreateTargetListRaw
|
|
311
|
+
);
|
|
295
312
|
const boardAssigneesEnabled =
|
|
296
313
|
!workspace.personal ||
|
|
297
314
|
board.access_type === 'guest' ||
|
|
@@ -820,6 +837,15 @@ export function BoardViews({
|
|
|
820
837
|
statusFilteredLists,
|
|
821
838
|
]);
|
|
822
839
|
|
|
840
|
+
useEffect(() => {
|
|
841
|
+
if (!hoveredTaskListId) return;
|
|
842
|
+
|
|
843
|
+
const stillSelectable = filteredLists.some(
|
|
844
|
+
(list) => list.id === hoveredTaskListId && !list.is_external_staging
|
|
845
|
+
);
|
|
846
|
+
if (!stillSelectable) setHoveredTaskListId(null);
|
|
847
|
+
}, [filteredLists, hoveredTaskListId]);
|
|
848
|
+
|
|
823
849
|
const sourceTasks = useMemo(() => {
|
|
824
850
|
if (localTaskState) return locallyFilteredTasks;
|
|
825
851
|
|
|
@@ -902,11 +928,14 @@ export function BoardViews({
|
|
|
902
928
|
const selectableLists = filteredLists.filter(
|
|
903
929
|
(list) => !list.is_external_staging
|
|
904
930
|
);
|
|
905
|
-
|
|
906
|
-
// to the first selectable list when unset or the list is unavailable.
|
|
907
|
-
const targetList =
|
|
931
|
+
const defaultTargetList =
|
|
908
932
|
selectableLists.find((list) => list.id === board.default_list_id) ??
|
|
909
933
|
selectableLists[0];
|
|
934
|
+
const hoveredTargetList =
|
|
935
|
+
quickCreateTargetList === 'hovered_list'
|
|
936
|
+
? selectableLists.find((list) => list.id === hoveredTaskListId)
|
|
937
|
+
: undefined;
|
|
938
|
+
const targetList = hoveredTargetList ?? defaultTargetList;
|
|
910
939
|
if (!targetList) return;
|
|
911
940
|
createTask(board.id, targetList.id, selectableLists, filters);
|
|
912
941
|
},
|
|
@@ -1022,6 +1051,7 @@ export function BoardViews({
|
|
|
1022
1051
|
onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
|
|
1023
1052
|
canUseBoardAssignees={boardAssigneesEnabled}
|
|
1024
1053
|
assigneeMemberSource={assigneeMemberSource}
|
|
1054
|
+
onHoveredTaskListChange={setHoveredTaskListId}
|
|
1025
1055
|
readOnly={readOnly}
|
|
1026
1056
|
/>
|
|
1027
1057
|
);
|
|
@@ -13,11 +13,11 @@ const HEADER_ACTIONS = [
|
|
|
13
13
|
'settings',
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
-
function TaskBoardHeaderSkeleton() {
|
|
16
|
+
function TaskBoardHeaderSkeleton({ root = false }: { root?: boolean }) {
|
|
17
17
|
return (
|
|
18
18
|
<div
|
|
19
19
|
aria-hidden="true"
|
|
20
|
-
className=
|
|
20
|
+
className={cn('border-b pt-2 pb-2', root ? 'pr-0 pl-2' : 'px-2')}
|
|
21
21
|
data-testid="task-board-header-skeleton"
|
|
22
22
|
>
|
|
23
23
|
<div className="flex flex-wrap items-center justify-between gap-1.5 sm:gap-2">
|
|
@@ -57,7 +57,7 @@ export function TaskBoardLoadingState({
|
|
|
57
57
|
className={cn(
|
|
58
58
|
'overflow-hidden bg-transparent',
|
|
59
59
|
root
|
|
60
|
-
? '-
|
|
60
|
+
? '-mt-4 -mb-4 -ml-4 h-[calc(100dvh+2rem)] min-h-[calc(32rem+2rem)] w-[calc(100%+2rem)] min-w-[calc(100%+2rem)]'
|
|
61
61
|
: 'h-[calc(100dvh-1rem)] min-h-[32rem] w-full',
|
|
62
62
|
showHeader && 'flex flex-col',
|
|
63
63
|
className
|
|
@@ -66,7 +66,7 @@ export function TaskBoardLoadingState({
|
|
|
66
66
|
>
|
|
67
67
|
{showHeader ? (
|
|
68
68
|
<>
|
|
69
|
-
<TaskBoardHeaderSkeleton />
|
|
69
|
+
<TaskBoardHeaderSkeleton root={root} />
|
|
70
70
|
<div
|
|
71
71
|
className="min-h-0 flex-1 overflow-hidden"
|
|
72
72
|
data-testid="task-board-loading-body"
|
|
@@ -542,6 +542,8 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
|
|
|
542
542
|
taskWsId={state.taskWsId}
|
|
543
543
|
task={state.task}
|
|
544
544
|
boardId={state.boardId || ''}
|
|
545
|
+
visibleBoardId={state.visibleBoardId}
|
|
546
|
+
visibleTaskSnapshot={state.visibleTaskSnapshot}
|
|
545
547
|
isOpen={state.isOpen}
|
|
546
548
|
availableLists={state.availableLists}
|
|
547
549
|
sharedContext={
|
|
@@ -220,6 +220,9 @@ interface TaskDialogHeaderProps {
|
|
|
220
220
|
isPersonalWorkspace?: boolean;
|
|
221
221
|
/** Callback to open share dialog */
|
|
222
222
|
onOpenShareDialog?: () => void;
|
|
223
|
+
/** Optional archive action for document-type tasks. */
|
|
224
|
+
onArchiveTask?: () => void;
|
|
225
|
+
archiveTaskDisabled?: boolean;
|
|
223
226
|
/** Whether the dialog is in read-only mode */
|
|
224
227
|
disabled?: boolean;
|
|
225
228
|
/** Whether task-dependent actions should be disabled while the task hydrates. */
|
|
@@ -263,6 +266,8 @@ export function TaskDialogHeader({
|
|
|
263
266
|
onNavigateBack,
|
|
264
267
|
isPersonalWorkspace = false,
|
|
265
268
|
onOpenShareDialog,
|
|
269
|
+
onArchiveTask,
|
|
270
|
+
archiveTaskDisabled = false,
|
|
266
271
|
disabled = false,
|
|
267
272
|
controlsDisabled = false,
|
|
268
273
|
onScrollToUserCursor,
|
|
@@ -487,6 +492,8 @@ export function TaskDialogHeader({
|
|
|
487
492
|
onShowDeleteDialog={() => setShowDeleteConfirm(true)}
|
|
488
493
|
onClearDraft={clearDraftState}
|
|
489
494
|
onNavigateBack={onNavigateBack}
|
|
495
|
+
onArchiveTask={onArchiveTask}
|
|
496
|
+
archiveTaskDisabled={archiveTaskDisabled}
|
|
490
497
|
onOpenShareDialog={onOpenShareDialog}
|
|
491
498
|
disabled={disabled}
|
|
492
499
|
controlsDisabled={controlsDisabled}
|