@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
|
@@ -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
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
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:
|
|
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 &&
|
|
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
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
249
|
-
canUseCurrentBoardLists &&
|
|
250
|
+
{canUseCurrentBoardLists &&
|
|
250
251
|
targetClosedList &&
|
|
251
252
|
targetClosedList.id !== task.list_id &&
|
|
252
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
252
|
-
<
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
<
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
</
|
|
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
|
-
<
|
|
323
|
+
<Tags className="h-3.5 w-3.5" />
|
|
313
324
|
<span>
|
|
314
|
-
{template.stats.
|
|
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
|
-
</
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|