@tuturuuu/ui 0.14.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 +78 -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-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/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +62 -0
- 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 +32 -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__/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/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-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/vitest.config.ts +4 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useRouter } from 'next/navigation';
|
|
4
4
|
import { useCallback, useMemo, useState } from 'react';
|
|
5
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../dialog';
|
|
5
6
|
import { useTasksHref } from '../tasks-route-context';
|
|
6
7
|
import {
|
|
7
8
|
ProjectGridCard,
|
|
@@ -16,11 +17,15 @@ import {
|
|
|
16
17
|
ManageTasksDialog,
|
|
17
18
|
} from './dialogs';
|
|
18
19
|
import { useProjectFilters, useTaskProjects } from './hooks';
|
|
20
|
+
import TaskProjectDetailPageClient from './projectId/task-project-detail-page-client';
|
|
19
21
|
import type { TaskProject, TaskProjectsClientProps } from './types';
|
|
20
22
|
|
|
21
23
|
export function TaskProjectsClient({
|
|
24
|
+
currentUserId,
|
|
25
|
+
detailMode = 'route',
|
|
22
26
|
wsId,
|
|
23
27
|
initialProjects,
|
|
28
|
+
workspace,
|
|
24
29
|
}: TaskProjectsClientProps) {
|
|
25
30
|
const router = useRouter();
|
|
26
31
|
const tasksHref = useTasksHref();
|
|
@@ -34,6 +39,9 @@ export function TaskProjectsClient({
|
|
|
34
39
|
const [managingProject, setManagingProject] = useState<TaskProject | null>(
|
|
35
40
|
null
|
|
36
41
|
);
|
|
42
|
+
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
|
|
43
|
+
null
|
|
44
|
+
);
|
|
37
45
|
|
|
38
46
|
// Custom hooks
|
|
39
47
|
const taskProjects = useTaskProjects({
|
|
@@ -74,6 +82,11 @@ export function TaskProjectsClient({
|
|
|
74
82
|
),
|
|
75
83
|
[taskProjects.projects]
|
|
76
84
|
);
|
|
85
|
+
const selectedProject = selectedProjectId
|
|
86
|
+
? (taskProjects.projects.find(
|
|
87
|
+
(project) => project.id === selectedProjectId
|
|
88
|
+
) ?? null)
|
|
89
|
+
: null;
|
|
77
90
|
|
|
78
91
|
// Handlers
|
|
79
92
|
const handleEditProject = (project: TaskProject) => {
|
|
@@ -120,9 +133,14 @@ export function TaskProjectsClient({
|
|
|
120
133
|
|
|
121
134
|
const navigateToProject = useCallback(
|
|
122
135
|
(projectId: string) => {
|
|
136
|
+
if (detailMode === 'dialog' && workspace && currentUserId) {
|
|
137
|
+
setSelectedProjectId(projectId);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
123
141
|
router.push(`/${wsId}${tasksHref(`/projects/${projectId}`)}`);
|
|
124
142
|
},
|
|
125
|
-
[router,
|
|
143
|
+
[currentUserId, detailMode, router, tasksHref, workspace, wsId]
|
|
126
144
|
);
|
|
127
145
|
|
|
128
146
|
return (
|
|
@@ -168,6 +186,7 @@ export function TaskProjectsClient({
|
|
|
168
186
|
onDelete={taskProjects.deleteProject}
|
|
169
187
|
onManageTasks={handleOpenManageTasks}
|
|
170
188
|
onNavigate={navigateToProject}
|
|
189
|
+
navigationMode={detailMode}
|
|
171
190
|
isUpdating={taskProjects.isUpdating}
|
|
172
191
|
isDeleting={taskProjects.isDeleting}
|
|
173
192
|
isLinking={taskProjects.isLinking}
|
|
@@ -186,6 +205,7 @@ export function TaskProjectsClient({
|
|
|
186
205
|
onDelete={taskProjects.deleteProject}
|
|
187
206
|
onManageTasks={handleOpenManageTasks}
|
|
188
207
|
onNavigate={navigateToProject}
|
|
208
|
+
navigationMode={detailMode}
|
|
189
209
|
isUpdating={taskProjects.isUpdating}
|
|
190
210
|
isDeleting={taskProjects.isDeleting}
|
|
191
211
|
isLinking={taskProjects.isLinking}
|
|
@@ -221,6 +241,30 @@ export function TaskProjectsClient({
|
|
|
221
241
|
isLinking={taskProjects.isLinking}
|
|
222
242
|
isUnlinking={taskProjects.isUnlinking}
|
|
223
243
|
/>
|
|
244
|
+
|
|
245
|
+
<Dialog
|
|
246
|
+
open={Boolean(selectedProjectId)}
|
|
247
|
+
onOpenChange={(open) => {
|
|
248
|
+
if (!open) setSelectedProjectId(null);
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
<DialogContent className="max-h-[90vh] max-w-6xl overflow-y-auto">
|
|
252
|
+
<DialogHeader>
|
|
253
|
+
<DialogTitle className="sr-only">
|
|
254
|
+
{selectedProject?.name ?? ''}
|
|
255
|
+
</DialogTitle>
|
|
256
|
+
</DialogHeader>
|
|
257
|
+
{selectedProjectId && workspace && currentUserId && (
|
|
258
|
+
<TaskProjectDetailPageClient
|
|
259
|
+
currentUserId={currentUserId}
|
|
260
|
+
embedded
|
|
261
|
+
projectId={selectedProjectId}
|
|
262
|
+
workspace={workspace}
|
|
263
|
+
wsId={wsId}
|
|
264
|
+
/>
|
|
265
|
+
)}
|
|
266
|
+
</DialogContent>
|
|
267
|
+
</Dialog>
|
|
224
268
|
</>
|
|
225
269
|
);
|
|
226
270
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TaskProjectLinkedItem } from '@tuturuuu/internal-api/tasks';
|
|
2
|
-
import type { TaskProjectWithRelations } from '@tuturuuu/types';
|
|
2
|
+
import type { TaskProjectWithRelations, Workspace } from '@tuturuuu/types';
|
|
3
3
|
|
|
4
4
|
export type ProjectStatus =
|
|
5
5
|
| 'backlog'
|
|
@@ -30,6 +30,9 @@ export interface TaskProject extends TaskProjectWithRelations {
|
|
|
30
30
|
export interface TaskProjectsClientProps {
|
|
31
31
|
wsId: string;
|
|
32
32
|
initialProjects: TaskProject[];
|
|
33
|
+
currentUserId?: string;
|
|
34
|
+
detailMode?: 'dialog' | 'route';
|
|
35
|
+
workspace?: Workspace | null;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export interface TaskOption {
|
|
@@ -70,6 +70,10 @@ interface TaskDialogState {
|
|
|
70
70
|
taskWorkspaceTier?: WorkspaceProductTier;
|
|
71
71
|
/** Initial board/list context used for immediate partial-task rendering. */
|
|
72
72
|
initialSharedContext?: SharedTaskContext;
|
|
73
|
+
/** Board that supplied the visible task card before source-task hydration. */
|
|
74
|
+
visibleBoardId?: string;
|
|
75
|
+
/** Visible task snapshot before hydration, used to preserve overlay metadata. */
|
|
76
|
+
visibleTaskSnapshot?: Partial<Task>;
|
|
73
77
|
/** True while an existing task was opened from a partial snapshot and is hydrating. */
|
|
74
78
|
isHydratingTask?: boolean;
|
|
75
79
|
/** True when the latest hydration request failed after the dialog already opened. */
|
|
@@ -90,6 +94,8 @@ interface OpenTaskByIdOptions {
|
|
|
90
94
|
canUseBoardAssignees?: boolean;
|
|
91
95
|
assigneeMemberSource?: TaskAssigneeMemberSource;
|
|
92
96
|
initialSharedContext?: SharedTaskContext;
|
|
97
|
+
visibleBoardId?: string;
|
|
98
|
+
visibleTaskSnapshot?: Partial<Task>;
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
interface TaskDialogContextValue {
|
|
@@ -486,6 +492,8 @@ export function TaskDialogProvider({
|
|
|
486
492
|
assigneeMemberSource: options?.assigneeMemberSource,
|
|
487
493
|
taskWorkspaceTier: options?.taskWorkspaceTier,
|
|
488
494
|
initialSharedContext: options?.initialSharedContext,
|
|
495
|
+
visibleBoardId: options?.visibleBoardId,
|
|
496
|
+
visibleTaskSnapshot: options?.visibleTaskSnapshot,
|
|
489
497
|
isHydratingTask: true,
|
|
490
498
|
taskLoadError: false,
|
|
491
499
|
taskHydrationVersion: 0,
|
|
@@ -567,6 +575,8 @@ export function TaskDialogProvider({
|
|
|
567
575
|
options?.canUseBoardAssignees ?? !isTaskWorkspacePersonal,
|
|
568
576
|
assigneeMemberSource: options?.assigneeMemberSource,
|
|
569
577
|
taskWorkspaceTier,
|
|
578
|
+
visibleBoardId: options?.visibleBoardId,
|
|
579
|
+
visibleTaskSnapshot: options?.visibleTaskSnapshot,
|
|
570
580
|
isHydratingTask: false,
|
|
571
581
|
taskLoadError: false,
|
|
572
582
|
taskHydrationVersion: 1,
|
|
@@ -209,10 +209,16 @@ describe('BoardClient', () => {
|
|
|
209
209
|
);
|
|
210
210
|
|
|
211
211
|
expect(screen.getByTestId('task-board-loading-state')).toHaveClass(
|
|
212
|
-
'-
|
|
212
|
+
'-mt-4',
|
|
213
|
+
'-mb-4',
|
|
214
|
+
'-ml-4',
|
|
213
215
|
'h-[calc(100dvh+2rem)]',
|
|
214
216
|
'w-[calc(100%+2rem)]'
|
|
215
217
|
);
|
|
218
|
+
expect(screen.getByTestId('task-board-loading-state')).not.toHaveClass(
|
|
219
|
+
'-m-4',
|
|
220
|
+
'-mr-4'
|
|
221
|
+
);
|
|
216
222
|
expect(
|
|
217
223
|
screen.getByTestId('task-board-header-skeleton')
|
|
218
224
|
).toBeInTheDocument();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import '@testing-library/jest-dom';
|
|
2
2
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
3
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { SidebarContext } from '@tuturuuu/ui/custom/sidebar-context';
|
|
4
5
|
import type React from 'react';
|
|
5
6
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
7
|
import { BoardHeader } from '../board-header';
|
|
@@ -180,7 +181,8 @@ const mockBoard = {
|
|
|
180
181
|
} as const;
|
|
181
182
|
|
|
182
183
|
function renderBoardHeader(
|
|
183
|
-
overrides?: Partial<React.ComponentProps<typeof BoardHeader
|
|
184
|
+
overrides?: Partial<React.ComponentProps<typeof BoardHeader>>,
|
|
185
|
+
sidebarContext?: React.ContextType<typeof SidebarContext>
|
|
184
186
|
) {
|
|
185
187
|
const queryClient = new QueryClient({
|
|
186
188
|
defaultOptions: {
|
|
@@ -190,7 +192,7 @@ function renderBoardHeader(
|
|
|
190
192
|
},
|
|
191
193
|
});
|
|
192
194
|
|
|
193
|
-
|
|
195
|
+
const content = (
|
|
194
196
|
<QueryClientProvider client={queryClient}>
|
|
195
197
|
<BoardHeader
|
|
196
198
|
workspaceId="ws-1"
|
|
@@ -221,6 +223,16 @@ function renderBoardHeader(
|
|
|
221
223
|
/>
|
|
222
224
|
</QueryClientProvider>
|
|
223
225
|
);
|
|
226
|
+
|
|
227
|
+
return render(
|
|
228
|
+
sidebarContext ? (
|
|
229
|
+
<SidebarContext.Provider value={sidebarContext}>
|
|
230
|
+
{content}
|
|
231
|
+
</SidebarContext.Provider>
|
|
232
|
+
) : (
|
|
233
|
+
content
|
|
234
|
+
)
|
|
235
|
+
);
|
|
224
236
|
}
|
|
225
237
|
|
|
226
238
|
describe('BoardHeader', () => {
|
|
@@ -247,6 +259,79 @@ describe('BoardHeader', () => {
|
|
|
247
259
|
);
|
|
248
260
|
});
|
|
249
261
|
|
|
262
|
+
it('renders a compact hide-sidebar button when sidebar context is available', () => {
|
|
263
|
+
const handleBehaviorChange = vi.fn();
|
|
264
|
+
|
|
265
|
+
renderBoardHeader(undefined, {
|
|
266
|
+
behavior: 'expanded',
|
|
267
|
+
handleBehaviorChange,
|
|
268
|
+
localOverride: false,
|
|
269
|
+
setBehavior: vi.fn(),
|
|
270
|
+
setLocalOverride: vi.fn(),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const hideButton = screen.getByRole('button', {
|
|
274
|
+
name: 'common.hide_sidebar',
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(hideButton).toHaveClass('border', 'h-7', 'w-7', 'px-0');
|
|
278
|
+
fireEvent.click(hideButton);
|
|
279
|
+
expect(handleBehaviorChange).toHaveBeenCalledWith('hidden');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('does not render the hide-sidebar button without context or when already hidden', () => {
|
|
283
|
+
const { rerender } = renderBoardHeader();
|
|
284
|
+
|
|
285
|
+
expect(
|
|
286
|
+
screen.queryByRole('button', { name: 'common.hide_sidebar' })
|
|
287
|
+
).not.toBeInTheDocument();
|
|
288
|
+
|
|
289
|
+
rerender(
|
|
290
|
+
<SidebarContext.Provider
|
|
291
|
+
value={{
|
|
292
|
+
behavior: 'hidden',
|
|
293
|
+
handleBehaviorChange: vi.fn(),
|
|
294
|
+
localOverride: false,
|
|
295
|
+
setBehavior: vi.fn(),
|
|
296
|
+
setLocalOverride: vi.fn(),
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
<QueryClientProvider client={new QueryClient()}>
|
|
300
|
+
<BoardHeader
|
|
301
|
+
workspaceId="ws-1"
|
|
302
|
+
board={mockBoard}
|
|
303
|
+
currentView="kanban"
|
|
304
|
+
filters={{
|
|
305
|
+
assignees: [],
|
|
306
|
+
dueDateRange: null,
|
|
307
|
+
estimationRange: null,
|
|
308
|
+
includeMyTasks: false,
|
|
309
|
+
includeUnassigned: false,
|
|
310
|
+
labels: [],
|
|
311
|
+
priorities: [],
|
|
312
|
+
projects: [],
|
|
313
|
+
sourceBoardIds: [],
|
|
314
|
+
sourceScope: 'all_visible',
|
|
315
|
+
sourceWorkspaceIds: [],
|
|
316
|
+
}}
|
|
317
|
+
isMultiSelectMode={false}
|
|
318
|
+
isPersonalWorkspace={false}
|
|
319
|
+
listStatusFilter="all"
|
|
320
|
+
onFiltersChange={vi.fn()}
|
|
321
|
+
onListStatusFilterChange={vi.fn()}
|
|
322
|
+
onUpdate={vi.fn()}
|
|
323
|
+
onViewChange={vi.fn()}
|
|
324
|
+
setIsMultiSelectMode={vi.fn()}
|
|
325
|
+
/>
|
|
326
|
+
</QueryClientProvider>
|
|
327
|
+
</SidebarContext.Provider>
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(
|
|
331
|
+
screen.queryByRole('button', { name: 'common.hide_sidebar' })
|
|
332
|
+
).not.toBeInTheDocument();
|
|
333
|
+
});
|
|
334
|
+
|
|
250
335
|
it('prefetches current board settings before opening the dialog', async () => {
|
|
251
336
|
renderBoardHeader();
|
|
252
337
|
|
|
@@ -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,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}
|