@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.
Files changed (96) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/biome.json +1 -1
  3. package/package.json +62 -62
  4. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +12 -0
  5. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +93 -6
  6. package/src/components/ui/custom/loading-statistic-card.tsx +18 -0
  7. package/src/components/ui/custom/notification-popover-client.tsx +5 -1
  8. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.test.tsx +34 -0
  9. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +106 -0
  10. package/src/components/ui/custom/settings/sidebar-settings.tsx +18 -6
  11. package/src/components/ui/custom/settings/task-settings.tsx +55 -0
  12. package/src/components/ui/custom/settings-dialog-shell.tsx +18 -12
  13. package/src/components/ui/custom/sidebar-context.tsx +58 -3
  14. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +5 -3
  15. package/src/components/ui/custom/statistic-card.tsx +104 -0
  16. package/src/components/ui/custom/structure.test.tsx +29 -0
  17. package/src/components/ui/custom/structure.tsx +103 -88
  18. package/src/components/ui/finance/invoices/civil-month.ts +224 -0
  19. package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.test.tsx +49 -0
  20. package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.tsx +81 -0
  21. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
  22. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +90 -2
  23. package/src/components/ui/finance/invoices/new-invoice-page.tsx +40 -4
  24. package/src/components/ui/finance/invoices/pending-columns.test.ts +26 -0
  25. package/src/components/ui/finance/invoices/pending-columns.tsx +45 -14
  26. package/src/components/ui/finance/invoices/standard-invoice.test.ts +31 -0
  27. package/src/components/ui/finance/invoices/standard-invoice.tsx +33 -16
  28. package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +331 -0
  29. package/src/components/ui/finance/invoices/subscription-invoice.tsx +53 -28
  30. package/src/components/ui/finance/invoices/utils.test.ts +84 -0
  31. package/src/components/ui/finance/invoices/utils.ts +56 -134
  32. package/src/components/ui/resizable.test.ts +15 -0
  33. package/src/components/ui/resizable.tsx +28 -6
  34. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +4 -0
  35. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +62 -0
  36. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.test.ts +118 -0
  37. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.ts +144 -70
  38. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +48 -0
  39. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +50 -6
  40. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +6 -0
  41. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +4 -2
  42. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +41 -3
  43. package/src/components/ui/tu-do/boards/boardId/menus/task-due-date-menu.tsx +31 -10
  44. package/src/components/ui/tu-do/boards/boardId/menus/task-priority-menu.tsx +7 -5
  45. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +7 -5
  46. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.test.ts +44 -0
  47. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.ts +17 -0
  48. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +2 -0
  49. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -0
  50. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +44 -30
  51. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +54 -0
  52. package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +43 -1
  53. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +33 -15
  54. package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +3 -3
  55. package/src/components/ui/tu-do/progress/task-progress-page.tsx +74 -28
  56. package/src/components/ui/tu-do/progress/task-progress-panels.tsx +12 -10
  57. package/src/components/ui/tu-do/projects/components/project-grid-card.tsx +22 -6
  58. package/src/components/ui/tu-do/projects/components/project-list-item.tsx +24 -8
  59. package/src/components/ui/tu-do/projects/projectId/components/__tests__/tasks-tab-layout.test.tsx +6 -0
  60. package/src/components/ui/tu-do/projects/projectId/components/tasks-tab.tsx +3 -1
  61. package/src/components/ui/tu-do/projects/projectId/task-project-detail-page-client.tsx +11 -3
  62. package/src/components/ui/tu-do/projects/projectId/task-project-detail.tsx +2 -0
  63. package/src/components/ui/tu-do/projects/projectId/types.ts +1 -0
  64. package/src/components/ui/tu-do/projects/task-projects-client.tsx +45 -1
  65. package/src/components/ui/tu-do/projects/types.ts +4 -1
  66. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +10 -0
  67. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -1
  68. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +87 -2
  69. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +70 -0
  70. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +27 -3
  71. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +41 -3
  72. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +54 -0
  73. package/src/components/ui/tu-do/shared/board-header.tsx +20 -0
  74. package/src/components/ui/tu-do/shared/board-views.tsx +33 -3
  75. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
  76. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +2 -0
  77. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +7 -0
  78. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-mutations.test.tsx +231 -0
  79. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +139 -68
  80. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.test.tsx +68 -0
  81. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +17 -0
  82. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +45 -23
  83. package/src/components/ui/tu-do/shared/task-quick-create-target-list.ts +20 -0
  84. package/src/components/ui/tu-do/shared/task-row-actions-menu.test.tsx +138 -0
  85. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +15 -9
  86. package/src/components/ui/tu-do/templates/client.tsx +97 -78
  87. package/src/components/ui/tu-do/templates/marketplace/client.tsx +171 -135
  88. package/src/components/ui/tu-do/templates/task-templates-hub.tsx +3 -0
  89. package/src/components/ui/tu-do/templates/templateId/client.tsx +24 -9
  90. package/src/components/ui/tu-do/templates/templateId/task-template-detail-page-client.tsx +8 -2
  91. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -0
  92. package/src/hooks/__tests__/use-task-actions.test.tsx +14 -39
  93. package/src/hooks/task-actions-personal-external.ts +12 -6
  94. package/src/hooks/use-notifications.ts +14 -2
  95. package/src/hooks/use-user-config.ts +10 -2
  96. package/vitest.config.ts +4 -0
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
+ Archive,
4
5
  ArrowLeft,
5
6
  Copy,
6
7
  ExternalLink,
@@ -45,6 +46,8 @@ interface TaskDialogActionsProps {
45
46
  onShowDeleteDialog: () => void;
46
47
  onClearDraft: () => void;
47
48
  onNavigateBack?: () => void;
49
+ onArchiveTask?: () => void;
50
+ archiveTaskDisabled?: boolean;
48
51
  onOpenShareDialog?: () => void;
49
52
  disabled?: boolean;
50
53
  controlsDisabled?: boolean;
@@ -65,6 +68,8 @@ export function TaskDialogActions({
65
68
  onShowDeleteDialog,
66
69
  onClearDraft,
67
70
  onNavigateBack,
71
+ onArchiveTask,
72
+ archiveTaskDisabled = false,
68
73
  onOpenShareDialog,
69
74
  disabled = false,
70
75
  controlsDisabled = false,
@@ -157,6 +162,18 @@ export function TaskDialogActions({
157
162
  <ExternalLink className="mr-2 h-4 w-4" />
158
163
  {t('common.copy_link')}
159
164
  </DropdownMenuItem>
165
+ {onArchiveTask && (
166
+ <DropdownMenuItem
167
+ disabled={archiveTaskDisabled}
168
+ onClick={() => {
169
+ onArchiveTask();
170
+ setIsMoreMenuOpen(false);
171
+ }}
172
+ >
173
+ <Archive className="mr-2 h-4 w-4" />
174
+ {t('common.archive')}
175
+ </DropdownMenuItem>
176
+ )}
160
177
  <DropdownMenuSeparator />
161
178
  <DropdownMenuItem
162
179
  onClick={() => {
@@ -160,6 +160,8 @@ export interface TaskEditDialogProps {
160
160
  taskWsId?: string;
161
161
  task?: Task;
162
162
  boardId: string;
163
+ visibleBoardId?: string;
164
+ visibleTaskSnapshot?: Partial<Task>;
163
165
  isOpen: boolean;
164
166
  /** Present when opened via /shared/task/[shareCode] */
165
167
  shareCode?: string;
@@ -205,6 +207,8 @@ export function TaskEditDialog({
205
207
  taskWsId,
206
208
  task,
207
209
  boardId,
210
+ visibleBoardId,
211
+ visibleTaskSnapshot,
208
212
  isOpen,
209
213
  shareCode,
210
214
  sharedPermission,
@@ -262,12 +266,9 @@ export function TaskEditDialog({
262
266
  const effectiveCollaborationMode =
263
267
  collaborationMode && !isHydratingTask && !taskLoadError;
264
268
 
265
- // Keep this permission query disabled when `disabled` shared-link mode is active
266
- // (`enabled` becomes false) so `canManageTaskMedia` stays undefined while
267
- // `isCheckingTaskMediaPermission` remains false for read-only visitors. That
268
- // allows `imageUploadHandler` to stay unset for shared-link views without
269
- // forcing a permission request that could re-enable upload affordances. The
270
- // query only runs when `effectiveTaskWsId` exists and `disabled` is false.
269
+ // Defer task media permission checks until an upload is requested. The dialog
270
+ // is often opened from satellite app sessions that can edit task fields but
271
+ // should not make a drive permission request unless the user uploads media.
271
272
  const { data: canManageTaskMedia, isPending: isCheckingTaskMediaPermission } =
272
273
  useQuery({
273
274
  queryKey: [
@@ -282,7 +283,7 @@ export function TaskEditDialog({
282
283
  );
283
284
  return result.hasPermission;
284
285
  },
285
- enabled: Boolean(effectiveTaskWsId) && !taskControlsDisabled,
286
+ enabled: false,
286
287
  staleTime: 5 * 60 * 1000,
287
288
  });
288
289
 
@@ -1012,6 +1013,8 @@ export function TaskEditDialog({
1012
1013
  taskId: task?.id,
1013
1014
  isCreateMode,
1014
1015
  boardId,
1016
+ visibleBoardId,
1017
+ visibleTaskSnapshot,
1015
1018
  estimationPoints: formState.estimationPoints ?? null,
1016
1019
  priority: formState.priority,
1017
1020
  selectedListId: formState.selectedListId,
@@ -1043,8 +1046,14 @@ export function TaskEditDialog({
1043
1046
  !disabled &&
1044
1047
  !taskLoadError;
1045
1048
  const compactEditActionsDisabled = isLoading || isHydratingTask;
1049
+ const isDocumentTask = currentList?.status === 'documents';
1046
1050
  const canShowCompactStatusActions =
1047
- canShowCompactEditActions && currentList?.status !== 'documents';
1051
+ canShowCompactEditActions && !isDocumentTask;
1052
+ const canShowArchiveAction =
1053
+ canShowCompactEditActions &&
1054
+ !!closedList &&
1055
+ closedList.id !== formState.selectedListId &&
1056
+ currentList?.status !== 'closed';
1048
1057
  const showCompactDoneAction =
1049
1058
  canShowCompactStatusActions &&
1050
1059
  !!doneList &&
@@ -1056,6 +1065,7 @@ export function TaskEditDialog({
1056
1065
  closedList.id !== formState.selectedListId &&
1057
1066
  closedList.id !== doneList?.id &&
1058
1067
  currentList?.status !== 'closed';
1068
+ const showDocumentArchiveAction = canShowArchiveAction && isDocumentTask;
1059
1069
 
1060
1070
  // Task relationships
1061
1071
  const {
@@ -1399,21 +1409,27 @@ export function TaskEditDialog({
1399
1409
 
1400
1410
  let hasPermission = canManageTaskMedia;
1401
1411
  if (hasPermission === undefined || isCheckingTaskMediaPermission) {
1402
- hasPermission = await queryClient.fetchQuery({
1403
- queryKey: [
1404
- 'workspace-permission',
1405
- effectiveTaskWsId,
1406
- 'manage_drive_tasks_directory',
1407
- ],
1408
- queryFn: async () => {
1409
- const result = await checkWorkspacePermission(
1412
+ try {
1413
+ hasPermission = await queryClient.fetchQuery({
1414
+ queryKey: [
1415
+ 'workspace-permission',
1410
1416
  effectiveTaskWsId,
1411
- 'manage_drive_tasks_directory'
1412
- );
1413
- return result.hasPermission;
1414
- },
1415
- staleTime: 5 * 60 * 1000,
1416
- });
1417
+ 'manage_drive_tasks_directory',
1418
+ ],
1419
+ queryFn: async () => {
1420
+ const result = await checkWorkspacePermission(
1421
+ effectiveTaskWsId,
1422
+ 'manage_drive_tasks_directory'
1423
+ );
1424
+ return result.hasPermission;
1425
+ },
1426
+ staleTime: 5 * 60 * 1000,
1427
+ });
1428
+ } catch {
1429
+ throw Object.assign(new Error(t('insufficient_permissions')), {
1430
+ code: 'INSUFFICIENT_PERMISSIONS',
1431
+ });
1432
+ }
1417
1433
  }
1418
1434
 
1419
1435
  if (!hasPermission) {
@@ -2417,7 +2433,7 @@ export function TaskEditDialog({
2417
2433
  <TooltipContent side="top">{t('mark_as_done')}</TooltipContent>
2418
2434
  </Tooltip>
2419
2435
  )}
2420
- {showCompactClosedAction && closedList && (
2436
+ {(showCompactClosedAction || showDocumentArchiveAction) && closedList && (
2421
2437
  <Tooltip>
2422
2438
  <TooltipTrigger asChild>
2423
2439
  <Button
@@ -2717,6 +2733,12 @@ export function TaskEditDialog({
2717
2733
  ? () => setShowShareDialog(true)
2718
2734
  : undefined
2719
2735
  }
2736
+ onArchiveTask={
2737
+ showDocumentArchiveAction && closedList
2738
+ ? () => void updateList(closedList.id)
2739
+ : undefined
2740
+ }
2741
+ archiveTaskDisabled={compactEditActionsDisabled}
2720
2742
  disabled={disabled}
2721
2743
  controlsDisabled={taskControlsDisabled}
2722
2744
  onScrollToUserCursor={
@@ -0,0 +1,20 @@
1
+ export const DEFAULT_TASK_QUICK_CREATE_TARGET_LIST = 'default_list';
2
+
3
+ export const TASK_QUICK_CREATE_TARGET_LIST_VALUES = [
4
+ DEFAULT_TASK_QUICK_CREATE_TARGET_LIST,
5
+ 'hovered_list',
6
+ ] as const;
7
+
8
+ export type TaskQuickCreateTargetList =
9
+ (typeof TASK_QUICK_CREATE_TARGET_LIST_VALUES)[number];
10
+
11
+ export function normalizeTaskQuickCreateTargetList(
12
+ value: string | null | undefined,
13
+ fallback: TaskQuickCreateTargetList = DEFAULT_TASK_QUICK_CREATE_TARGET_LIST
14
+ ): TaskQuickCreateTargetList {
15
+ return TASK_QUICK_CREATE_TARGET_LIST_VALUES.includes(
16
+ value as TaskQuickCreateTargetList
17
+ )
18
+ ? (value as TaskQuickCreateTargetList)
19
+ : fallback;
20
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { render, screen } from '@testing-library/react';
6
+ import type { Task } from '@tuturuuu/types/primitives/Task';
7
+ import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
8
+ import type { ReactNode } from 'react';
9
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
10
+ import { TaskRowActionsMenu } from './task-row-actions-menu';
11
+
12
+ const { handleMoveToCloseMock, openTaskMock, useTaskActionsMock } = vi.hoisted(
13
+ () => ({
14
+ handleMoveToCloseMock: vi.fn(),
15
+ openTaskMock: vi.fn(),
16
+ useTaskActionsMock: vi.fn(),
17
+ })
18
+ );
19
+
20
+ vi.mock('next-intl', () => ({
21
+ useTranslations: () => (key: string) => key,
22
+ }));
23
+
24
+ vi.mock('next/link', () => ({
25
+ default: ({ children, href }: { children: ReactNode; href: string }) => (
26
+ <a href={href}>{children}</a>
27
+ ),
28
+ }));
29
+
30
+ vi.mock('@tuturuuu/ui/hooks/use-task-actions', () => ({
31
+ useTaskActions: () => useTaskActionsMock(),
32
+ }));
33
+
34
+ vi.mock('../hooks/useTaskDialog', () => ({
35
+ useTaskDialog: () => ({ openTask: openTaskMock }),
36
+ }));
37
+
38
+ vi.mock('../boards/boardId/menus', () => ({
39
+ TaskDueDateMenu: ({
40
+ translations,
41
+ }: {
42
+ translations: { dueDate: string };
43
+ }) => <div>{translations.dueDate}</div>,
44
+ TaskMoveMenu: ({ translations }: { translations: { move: string } }) => (
45
+ <div>{translations.move}</div>
46
+ ),
47
+ TaskPriorityMenu: ({
48
+ translations,
49
+ }: {
50
+ translations: { priority: string };
51
+ }) => <div>{translations.priority}</div>,
52
+ TaskSchedulingMenu: ({
53
+ translations,
54
+ }: {
55
+ translations: { schedule: string };
56
+ }) => <div>{translations.schedule}</div>,
57
+ }));
58
+
59
+ const documentList = {
60
+ id: 'documents-list',
61
+ name: 'Documents',
62
+ board_id: 'board-1',
63
+ status: 'documents',
64
+ created_at: '2026-07-01T00:00:00.000Z',
65
+ deleted: false,
66
+ } as TaskList;
67
+
68
+ const closedList = {
69
+ id: 'closed-list',
70
+ name: 'Closed',
71
+ board_id: 'board-1',
72
+ status: 'closed',
73
+ created_at: '2026-07-01T00:00:00.000Z',
74
+ deleted: false,
75
+ } as TaskList;
76
+
77
+ const task = {
78
+ id: 'task-1',
79
+ name: 'Document task',
80
+ list_id: documentList.id,
81
+ created_at: '2026-07-01T00:00:00.000Z',
82
+ display_number: 1,
83
+ } as Task;
84
+
85
+ describe('TaskRowActionsMenu document tasks', () => {
86
+ beforeEach(() => {
87
+ vi.clearAllMocks();
88
+ useTaskActionsMock.mockReturnValue({
89
+ handleDelete: vi.fn(),
90
+ handleDueDateChange: vi.fn(),
91
+ handleMoveToClose: handleMoveToCloseMock,
92
+ handleMoveToCompletion: vi.fn(),
93
+ handleMoveToList: vi.fn(),
94
+ handlePriorityChange: vi.fn(),
95
+ });
96
+ });
97
+
98
+ it('shows Archive for document-list tasks while hiding workflow-only actions', () => {
99
+ render(
100
+ <TaskRowActionsMenu
101
+ boardId="board-1"
102
+ lists={[documentList, closedList]}
103
+ onOpenChange={vi.fn()}
104
+ onUpdate={vi.fn()}
105
+ open
106
+ task={task}
107
+ trigger={<button type="button">Actions</button>}
108
+ workspaceId="workspace-1"
109
+ />
110
+ );
111
+
112
+ expect(screen.getByText('archive')).toBeInTheDocument();
113
+ expect(screen.queryByText('mark_as_done')).not.toBeInTheDocument();
114
+ expect(screen.queryByText('mark_as_closed')).not.toBeInTheDocument();
115
+ expect(screen.queryByText('start_tracking_time')).not.toBeInTheDocument();
116
+ expect(screen.queryByText('due_date')).not.toBeInTheDocument();
117
+ expect(
118
+ screen.queryByText('ws-task-boards.dialog.schedule')
119
+ ).not.toBeInTheDocument();
120
+ });
121
+
122
+ it('hides Archive for document-list tasks when the board has no closed list', () => {
123
+ render(
124
+ <TaskRowActionsMenu
125
+ boardId="board-1"
126
+ lists={[documentList]}
127
+ onOpenChange={vi.fn()}
128
+ onUpdate={vi.fn()}
129
+ open
130
+ task={task}
131
+ trigger={<button type="button">Actions</button>}
132
+ workspaceId="workspace-1"
133
+ />
134
+ );
135
+
136
+ expect(screen.queryByText('archive')).not.toBeInTheDocument();
137
+ });
138
+ });
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
+ Archive,
4
5
  CalendarDays,
5
6
  ExternalLink,
6
7
  MoreHorizontal,
@@ -104,6 +105,7 @@ export function TaskRowActionsMenu({
104
105
  const taskList = lists.find((list) => list.id === task.list_id);
105
106
  const targetCompletionList = useMemo(() => getCompletionList(lists), [lists]);
106
107
  const targetClosedList = useMemo(() => getClosedList(lists), [lists]);
108
+ const isDocumentList = taskList?.status === 'documents';
107
109
  const sourceWorkspaceId = task.source_workspace_id ?? workspaceId;
108
110
  const sourceBoardId = task.source_board_id ?? boardId;
109
111
  const isCrossWorkspaceExternal =
@@ -204,7 +206,7 @@ export function TaskRowActionsMenu({
204
206
  <Pencil className="h-4 w-4" />
205
207
  {t('edit')}
206
208
  </DropdownMenuItem>
207
- {taskList?.status !== 'documents' && (
209
+ {!isDocumentList && (
208
210
  <DropdownMenuItem asChild>
209
211
  <Link
210
212
  href={`/${sourceWorkspaceId}/time-tracker/timer?taskSelect=${task.id}`}
@@ -225,7 +227,7 @@ export function TaskRowActionsMenu({
225
227
  </DropdownMenuItem>
226
228
  )}
227
229
  <DropdownMenuSeparator />
228
- {taskList?.status !== 'documents' &&
230
+ {!isDocumentList &&
229
231
  canUseCurrentBoardLists &&
230
232
  targetCompletionList &&
231
233
  targetCompletionList.id !== task.list_id && (
@@ -245,11 +247,11 @@ export function TaskRowActionsMenu({
245
247
  : t('mark_as_closed')}
246
248
  </DropdownMenuItem>
247
249
  )}
248
- {taskList?.status !== 'documents' &&
249
- canUseCurrentBoardLists &&
250
+ {canUseCurrentBoardLists &&
250
251
  targetClosedList &&
251
252
  targetClosedList.id !== task.list_id &&
252
- targetClosedList.id !== targetCompletionList?.id && (
253
+ (isDocumentList ||
254
+ targetClosedList.id !== targetCompletionList?.id) && (
253
255
  <DropdownMenuItem
254
256
  onSelect={(event) =>
255
257
  handleMenuItemSelect(
@@ -260,8 +262,12 @@ export function TaskRowActionsMenu({
260
262
  className="cursor-pointer"
261
263
  disabled={isLoading}
262
264
  >
263
- <CalendarDays className="h-4 w-4 text-dynamic-purple" />
264
- {t('mark_as_closed')}
265
+ {isDocumentList ? (
266
+ <Archive className="h-4 w-4 text-dynamic-purple" />
267
+ ) : (
268
+ <CalendarDays className="h-4 w-4 text-dynamic-purple" />
269
+ )}
270
+ {isDocumentList ? t('archive') : t('mark_as_closed')}
265
271
  </DropdownMenuItem>
266
272
  )}
267
273
  <TaskPriorityMenu
@@ -279,7 +285,7 @@ export function TaskRowActionsMenu({
279
285
  low: t('priority_low'),
280
286
  }}
281
287
  />
282
- {taskList?.status !== 'documents' && (
288
+ {!isDocumentList && (
283
289
  <TaskDueDateMenu
284
290
  endDate={task.end_date}
285
291
  isLoading={isLoading}
@@ -300,7 +306,7 @@ export function TaskRowActionsMenu({
300
306
  }}
301
307
  />
302
308
  )}
303
- {taskList?.status !== 'documents' && (
309
+ {!isDocumentList && (
304
310
  <TaskSchedulingMenu
305
311
  task={task}
306
312
  boardId={boardId}
@@ -52,6 +52,7 @@ import type { BoardTemplate, TemplateFilter } from './types';
52
52
  interface Props {
53
53
  wsId: string;
54
54
  initialTemplates: BoardTemplate[];
55
+ onOpenTemplate?: (template: BoardTemplate) => void;
55
56
  templatesBasePath?: string;
56
57
  }
57
58
 
@@ -60,6 +61,7 @@ type TemplateSourceBoard = Pick<WorkspaceTaskBoard, 'id' | 'name' | 'ws_id'>;
60
61
  export default function TemplatesClient({
61
62
  wsId,
62
63
  initialTemplates,
64
+ onOpenTemplate,
63
65
  templatesBasePath = 'templates',
64
66
  }: Props) {
65
67
  const t = useTranslations('ws-board-templates');
@@ -181,6 +183,7 @@ export default function TemplatesClient({
181
183
  {filteredTemplates.map((template) => (
182
184
  <TemplateCard
183
185
  key={template.id}
186
+ onOpenTemplate={onOpenTemplate}
184
187
  template={template}
185
188
  wsId={wsId}
186
189
  templatesBasePath={templatesBasePath}
@@ -225,12 +228,14 @@ export default function TemplatesClient({
225
228
  }
226
229
 
227
230
  interface TemplateCardProps {
231
+ onOpenTemplate?: (template: BoardTemplate) => void;
228
232
  template: BoardTemplate;
229
233
  wsId: string;
230
234
  templatesBasePath: string;
231
235
  }
232
236
 
233
237
  function TemplateCard({
238
+ onOpenTemplate,
234
239
  template,
235
240
  wsId,
236
241
  templatesBasePath,
@@ -248,92 +253,106 @@ function TemplateCard({
248
253
 
249
254
  const visibilityLabel = t(`visibility.${template.visibility}`);
250
255
 
251
- return (
252
- <Link href={`/${wsId}/${templatesBasePath}/${template.id}`}>
253
- <Card className="h-full overflow-hidden transition-all hover:border-primary/50 hover:shadow-md">
254
- <div className="aspect-video w-full overflow-hidden border-b bg-muted/30">
255
- {template.backgroundUrl ? (
256
- <Image
257
- src={template.backgroundUrl}
258
- alt={template.name}
259
- width={400}
260
- height={225}
261
- className="h-full w-full object-cover transition-transform duration-300 hover:scale-105"
262
- />
263
- ) : (
264
- <div className="flex h-full w-full items-center justify-center">
265
- <KanbanSquare className="h-10 w-10 text-muted-foreground/20" />
256
+ const card = (
257
+ <Card className="h-full overflow-hidden transition-all hover:border-primary/50 hover:shadow-md">
258
+ <div className="aspect-video w-full overflow-hidden border-b bg-muted/30">
259
+ {template.backgroundUrl ? (
260
+ <Image
261
+ src={template.backgroundUrl}
262
+ alt={template.name}
263
+ width={400}
264
+ height={225}
265
+ className="h-full w-full object-cover transition-transform duration-300 hover:scale-105"
266
+ />
267
+ ) : (
268
+ <div className="flex h-full w-full items-center justify-center">
269
+ <KanbanSquare className="h-10 w-10 text-muted-foreground/20" />
270
+ </div>
271
+ )}
272
+ </div>
273
+ <CardHeader className="pb-3">
274
+ <div className="flex items-start justify-between gap-2">
275
+ <div className="flex items-center gap-2">
276
+ <div className="rounded-md bg-primary/10 p-2">
277
+ <Bookmark className="h-4 w-4 text-primary" />
266
278
  </div>
267
- )}
268
- </div>
269
- <CardHeader className="pb-3">
270
- <div className="flex items-start justify-between gap-2">
271
- <div className="flex items-center gap-2">
272
- <div className="rounded-md bg-primary/10 p-2">
273
- <Bookmark className="h-4 w-4 text-primary" />
274
- </div>
275
- <div className="min-w-0 flex-1">
276
- <CardTitle className="truncate text-base">
277
- {template.name}
278
- </CardTitle>
279
- </div>
279
+ <div className="min-w-0 flex-1">
280
+ <CardTitle className="truncate text-base">
281
+ {template.name}
282
+ </CardTitle>
280
283
  </div>
281
- <Badge
282
- variant="outline"
283
- className={cn(
284
- 'flex shrink-0 items-center gap-1 text-xs',
285
- template.visibility === 'private' &&
286
- 'border-dynamic-orange/30 text-dynamic-orange',
287
- template.visibility === 'workspace' &&
288
- 'border-dynamic-blue/30 text-dynamic-blue',
289
- template.visibility === 'public' &&
290
- 'border-dynamic-green/30 text-dynamic-green'
291
- )}
292
- >
293
- {visibilityIcon}
294
- <span className="hidden sm:inline">{visibilityLabel}</span>
295
- </Badge>
296
284
  </div>
297
- {template.description && (
298
- <CardDescription className="line-clamp-2 text-sm">
299
- {template.description}
300
- </CardDescription>
301
- )}
302
- </CardHeader>
303
- <CardContent className="pt-0">
304
- <div className="flex flex-wrap items-center gap-3 text-muted-foreground text-xs">
305
- <div className="flex items-center gap-1">
306
- <KanbanSquare className="h-3.5 w-3.5" />
307
- <span>
308
- {template.stats.lists} {t('gallery.lists')}
309
- </span>
310
- </div>
285
+ <Badge
286
+ variant="outline"
287
+ className={cn(
288
+ 'flex shrink-0 items-center gap-1 text-xs',
289
+ template.visibility === 'private' &&
290
+ 'border-dynamic-orange/30 text-dynamic-orange',
291
+ template.visibility === 'workspace' &&
292
+ 'border-dynamic-blue/30 text-dynamic-blue',
293
+ template.visibility === 'public' &&
294
+ 'border-dynamic-green/30 text-dynamic-green'
295
+ )}
296
+ >
297
+ {visibilityIcon}
298
+ <span className="hidden sm:inline">{visibilityLabel}</span>
299
+ </Badge>
300
+ </div>
301
+ {template.description && (
302
+ <CardDescription className="line-clamp-2 text-sm">
303
+ {template.description}
304
+ </CardDescription>
305
+ )}
306
+ </CardHeader>
307
+ <CardContent className="pt-0">
308
+ <div className="flex flex-wrap items-center gap-3 text-muted-foreground text-xs">
309
+ <div className="flex items-center gap-1">
310
+ <KanbanSquare className="h-3.5 w-3.5" />
311
+ <span>
312
+ {template.stats.lists} {t('gallery.lists')}
313
+ </span>
314
+ </div>
315
+ <div className="flex items-center gap-1">
316
+ <ListTodo className="h-3.5 w-3.5" />
317
+ <span>
318
+ {template.stats.tasks} {t('gallery.tasks')}
319
+ </span>
320
+ </div>
321
+ {template.stats.labels > 0 && (
311
322
  <div className="flex items-center gap-1">
312
- <ListTodo className="h-3.5 w-3.5" />
323
+ <Tags className="h-3.5 w-3.5" />
313
324
  <span>
314
- {template.stats.tasks} {t('gallery.tasks')}
325
+ {template.stats.labels} {t('gallery.labels')}
315
326
  </span>
316
327
  </div>
317
- {template.stats.labels > 0 && (
318
- <div className="flex items-center gap-1">
319
- <Tags className="h-3.5 w-3.5" />
320
- <span>
321
- {template.stats.labels} {t('gallery.labels')}
322
- </span>
323
- </div>
324
- )}
325
- </div>
326
- {template.isOwner && (
327
- <Badge
328
- variant="secondary"
329
- className="mt-3 bg-primary/10 text-primary text-xs"
330
- >
331
- {t('gallery.owner_badge')}
332
- </Badge>
333
328
  )}
334
- </CardContent>
335
- </Card>
336
- </Link>
329
+ </div>
330
+ {template.isOwner && (
331
+ <Badge
332
+ variant="secondary"
333
+ className="mt-3 bg-primary/10 text-primary text-xs"
334
+ >
335
+ {t('gallery.owner_badge')}
336
+ </Badge>
337
+ )}
338
+ </CardContent>
339
+ </Card>
340
+ );
341
+
342
+ if (onOpenTemplate) {
343
+ return (
344
+ <button
345
+ type="button"
346
+ className="block h-full w-full text-left"
347
+ onClick={() => onOpenTemplate(template)}
348
+ >
349
+ {card}
350
+ </button>
351
+ );
352
+ }
353
+
354
+ return (
355
+ <Link href={`/${wsId}/${templatesBasePath}/${template.id}`}>{card}</Link>
337
356
  );
338
357
  }
339
358