@tuturuuu/ui 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/biome.json +1 -1
- package/package.json +74 -71
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +1 -1
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
- package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
- package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
- package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/carousel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
- package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
- package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/command.tsx +1 -1
- package/src/components/ui/context-menu.tsx +5 -1
- package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +46 -1
- package/src/components/ui/custom/combobox.test.tsx +195 -0
- package/src/components/ui/custom/combobox.tsx +273 -156
- package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
- package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
- package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
- package/src/components/ui/custom/nav-link.test.tsx +165 -0
- package/src/components/ui/custom/nav-link.tsx +69 -11
- package/src/components/ui/custom/navigation.tsx +1 -0
- package/src/components/ui/custom/settings/task-settings.tsx +104 -0
- package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
- package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
- package/src/components/ui/custom/settings-dialog-search.ts +75 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +65 -28
- package/src/components/ui/custom/theme-toggle.tsx +1 -1
- package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
- package/src/components/ui/custom/workspace-select.tsx +25 -19
- package/src/components/ui/dialog.test.tsx +52 -0
- package/src/components/ui/dialog.tsx +6 -2
- package/src/components/ui/dropdown-menu.tsx +5 -1
- package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
- package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
- package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
- package/src/components/ui/finance/debts/debts-page.tsx +15 -2
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
- package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/utils.ts +3 -1
- package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
- package/src/components/ui/finance/transactions/form-types.ts +1 -0
- package/src/components/ui/finance/transactions/form.tsx +2 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
- package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
- package/src/components/ui/finance/wallets/form.test.tsx +51 -3
- package/src/components/ui/finance/wallets/form.tsx +15 -4
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
- package/src/components/ui/input-otp.tsx +1 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
- package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
- package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
- package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
- package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
- package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
- package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
- package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
- package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
- package/src/components/ui/navigation-menu.tsx +1 -1
- package/src/components/ui/pagination.tsx +1 -1
- package/src/components/ui/radio-group.tsx +1 -1
- package/src/components/ui/select.tsx +5 -1
- package/src/components/ui/sheet.tsx +1 -1
- package/src/components/ui/sidebar.tsx +1 -1
- package/src/components/ui/storefront/cart-popover.tsx +61 -0
- package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
- package/src/components/ui/storefront/cart-summary.tsx +93 -154
- package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
- package/src/components/ui/storefront/listing-card.tsx +1 -1
- package/src/components/ui/storefront/merch-sections.tsx +70 -0
- package/src/components/ui/storefront/product-detail.tsx +1 -1
- package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
- package/src/components/ui/storefront/storefront-surface.tsx +101 -166
- package/src/components/ui/storefront/types.ts +4 -0
- package/src/components/ui/storefront/utils.ts +6 -0
- package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
- package/src/components/ui/text-editor/background-color-extension.ts +62 -0
- package/src/components/ui/text-editor/color-controls.tsx +284 -0
- package/src/components/ui/text-editor/editor.tsx +69 -14
- package/src/components/ui/text-editor/extensions.ts +8 -2
- package/src/components/ui/text-editor/highlight-extension.ts +22 -0
- package/src/components/ui/text-editor/tool-bar.tsx +9 -16
- package/src/components/ui/toast.tsx +1 -1
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +286 -0
- package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
- package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +15 -226
- package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +121 -39
- package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +642 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +224 -15
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +535 -53
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +101 -33
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +235 -113
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +50 -5
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +12 -2
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +10 -1
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +271 -36
- package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
- package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +22 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
- package/src/components/ui/tu-do/boards/form.tsx +1 -1
- package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
- package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
- package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
- package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
- package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
- package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
- package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
- package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
- package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
- package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
- package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
- package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +141 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +377 -36
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +374 -0
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +419 -5
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +38 -0
- package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
- package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
- package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +15 -10
- package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +471 -975
- package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
- package/src/components/ui/tu-do/shared/board-switcher.tsx +244 -220
- package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
- package/src/components/ui/tu-do/shared/board-views.tsx +577 -85
- package/src/components/ui/tu-do/shared/list-view.tsx +246 -2
- package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
- package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
- package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +11 -1
- package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
- package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
- package/src/declarations.d.ts +1 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
- package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
- package/src/hooks/use-calendar-sync.tsx +247 -243
- package/src/hooks/use-calendar.tsx +323 -138
- package/src/hooks/use-task-actions.ts +24 -0
- package/src/hooks/use-user-workspace-config.ts +75 -0
- package/src/hooks/use-workspace-currency.ts +8 -3
- package/src/hooks/useBoardPresence.ts +364 -0
- package/src/hooks/useBoardRealtimeEventHandler.ts +45 -90
- package/src/lib/workspace-actions.ts +2 -6
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
6
|
+
import { act, renderHook } from '@testing-library/react';
|
|
7
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
8
|
+
import type { ReactNode } from 'react';
|
|
9
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { useTaskProjectManagement } from '../useTaskProjectManagement';
|
|
11
|
+
|
|
12
|
+
vi.mock('@tuturuuu/ui/sonner', () => ({
|
|
13
|
+
toast: {
|
|
14
|
+
error: vi.fn(),
|
|
15
|
+
success: vi.fn(),
|
|
16
|
+
warning: vi.fn(),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@tuturuuu/internal-api/tasks', () => ({
|
|
21
|
+
createWorkspaceTaskProject: vi.fn(),
|
|
22
|
+
updateWorkspaceTask: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
describe('useTaskProjectManagement', () => {
|
|
26
|
+
let queryClient: QueryClient;
|
|
27
|
+
let mockUpdateWorkspaceTask: any;
|
|
28
|
+
|
|
29
|
+
const project = {
|
|
30
|
+
id: 'project-1',
|
|
31
|
+
name: 'Roadmap',
|
|
32
|
+
status: 'active',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const mockTask = {
|
|
36
|
+
id: 'task-1',
|
|
37
|
+
name: 'Test Task',
|
|
38
|
+
list_id: 'list-1',
|
|
39
|
+
display_number: 1,
|
|
40
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
41
|
+
projects: [],
|
|
42
|
+
} satisfies Task;
|
|
43
|
+
|
|
44
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
45
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
beforeEach(async () => {
|
|
49
|
+
queryClient = new QueryClient({
|
|
50
|
+
defaultOptions: {
|
|
51
|
+
queries: { retry: false },
|
|
52
|
+
mutations: { retry: false },
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { updateWorkspaceTask } = await import(
|
|
57
|
+
'@tuturuuu/internal-api/tasks'
|
|
58
|
+
);
|
|
59
|
+
mockUpdateWorkspaceTask = updateWorkspaceTask as any;
|
|
60
|
+
|
|
61
|
+
vi.clearAllMocks();
|
|
62
|
+
mockUpdateWorkspaceTask.mockResolvedValue({ task: { id: 'task-1' } });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('updates full-board and task detail caches without invalidating visible board queries', async () => {
|
|
66
|
+
queryClient.setQueryData(['tasks', 'board-1'], [mockTask]);
|
|
67
|
+
queryClient.setQueryData(['tasks-full', 'board-1', 'all'], [mockTask]);
|
|
68
|
+
queryClient.setQueryData(['task', 'task-1'], mockTask);
|
|
69
|
+
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
70
|
+
|
|
71
|
+
const { result } = renderHook(
|
|
72
|
+
() =>
|
|
73
|
+
useTaskProjectManagement({
|
|
74
|
+
task: mockTask,
|
|
75
|
+
boardId: 'board-1',
|
|
76
|
+
workspaceProjects: [project],
|
|
77
|
+
workspaceId: 'ws-1',
|
|
78
|
+
taskId: 'task-1',
|
|
79
|
+
}),
|
|
80
|
+
{ wrapper }
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
await act(async () => {
|
|
84
|
+
await result.current.toggleTaskProject('project-1');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(
|
|
88
|
+
queryClient
|
|
89
|
+
.getQueryData<Task[]>(['tasks-full', 'board-1', 'all'])?.[0]
|
|
90
|
+
?.projects?.some((entry) => entry.id === 'project-1')
|
|
91
|
+
).toBe(true);
|
|
92
|
+
expect(
|
|
93
|
+
queryClient
|
|
94
|
+
.getQueryData<Task>(['task', 'task-1'])
|
|
95
|
+
?.projects?.some((entry) => entry.id === 'project-1')
|
|
96
|
+
).toBe(true);
|
|
97
|
+
expect(invalidateSpy).not.toHaveBeenCalledWith({
|
|
98
|
+
queryKey: ['tasks', 'board-1'],
|
|
99
|
+
});
|
|
100
|
+
expect(invalidateSpy).not.toHaveBeenCalledWith({
|
|
101
|
+
queryKey: ['tasks-full', 'board-1'],
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('rolls back every visible cache when the project update fails', async () => {
|
|
106
|
+
const taskWithProject = {
|
|
107
|
+
...mockTask,
|
|
108
|
+
projects: [project],
|
|
109
|
+
} as Task;
|
|
110
|
+
mockUpdateWorkspaceTask.mockRejectedValueOnce(new Error('Database error'));
|
|
111
|
+
queryClient.setQueryData(['tasks', 'board-1'], [taskWithProject]);
|
|
112
|
+
queryClient.setQueryData(
|
|
113
|
+
['tasks-full', 'board-1', 'all'],
|
|
114
|
+
[taskWithProject]
|
|
115
|
+
);
|
|
116
|
+
queryClient.setQueryData(['task', 'task-1'], taskWithProject);
|
|
117
|
+
|
|
118
|
+
const { result } = renderHook(
|
|
119
|
+
() =>
|
|
120
|
+
useTaskProjectManagement({
|
|
121
|
+
task: taskWithProject,
|
|
122
|
+
boardId: 'board-1',
|
|
123
|
+
workspaceProjects: [project],
|
|
124
|
+
workspaceId: 'ws-1',
|
|
125
|
+
taskId: 'task-1',
|
|
126
|
+
}),
|
|
127
|
+
{ wrapper }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
await act(async () => {
|
|
131
|
+
await result.current.toggleTaskProject('project-1');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(queryClient.getQueryData<Task[]>(['tasks', 'board-1'])).toEqual([
|
|
135
|
+
taskWithProject,
|
|
136
|
+
]);
|
|
137
|
+
expect(
|
|
138
|
+
queryClient.getQueryData<Task[]>(['tasks-full', 'board-1', 'all'])
|
|
139
|
+
).toEqual([taskWithProject]);
|
|
140
|
+
expect(queryClient.getQueryData<Task>(['task', 'task-1'])).toEqual(
|
|
141
|
+
taskWithProject
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -4,6 +4,7 @@ import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
|
4
4
|
import type { TaskFilters } from '@tuturuuu/ui/tu-do/boards/boardId/task-filter';
|
|
5
5
|
import {
|
|
6
6
|
type PendingRelationshipType,
|
|
7
|
+
type TaskAssigneeMemberSource,
|
|
7
8
|
useTaskDialogContext,
|
|
8
9
|
} from '../providers/task-dialog-provider';
|
|
9
10
|
import type { SharedTaskContext } from '../shared/task-edit-dialog/hooks/use-task-data';
|
|
@@ -50,6 +51,10 @@ export function useTaskDialog(): {
|
|
|
50
51
|
taskWsId?: string;
|
|
51
52
|
/** Whether the task's workspace is personal (affects realtime features) */
|
|
52
53
|
taskWorkspacePersonal?: boolean;
|
|
54
|
+
/** Whether the board context should expose assignee controls */
|
|
55
|
+
canUseBoardAssignees?: boolean;
|
|
56
|
+
/** Where assignee candidates should be loaded from */
|
|
57
|
+
assigneeMemberSource?: TaskAssigneeMemberSource;
|
|
53
58
|
}
|
|
54
59
|
) => void;
|
|
55
60
|
openTaskById: (
|
|
@@ -62,6 +67,8 @@ export function useTaskDialog(): {
|
|
|
62
67
|
taskWsId?: string;
|
|
63
68
|
taskWorkspacePersonal?: boolean;
|
|
64
69
|
taskWorkspaceTier?: WorkspaceProductTier;
|
|
70
|
+
canUseBoardAssignees?: boolean;
|
|
71
|
+
assigneeMemberSource?: TaskAssigneeMemberSource;
|
|
65
72
|
initialSharedContext?: SharedTaskContext;
|
|
66
73
|
}
|
|
67
74
|
) => Promise<boolean>;
|
|
@@ -9,7 +9,17 @@ import type { TaskLabel as DbTaskLabel } from '@tuturuuu/types/db';
|
|
|
9
9
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
10
10
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
11
11
|
import { useState } from 'react';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getActiveBoardRefresh,
|
|
14
|
+
useBoardBroadcast,
|
|
15
|
+
} from '../shared/board-broadcast-context';
|
|
16
|
+
import {
|
|
17
|
+
getTaskFromVisibleCaches,
|
|
18
|
+
patchTaskInVisibleCaches,
|
|
19
|
+
restoreTasksFromVisibleCacheSnapshot,
|
|
20
|
+
restoreVisibleTaskCaches,
|
|
21
|
+
snapshotVisibleTaskCaches,
|
|
22
|
+
} from '../shared/task-cache-patches';
|
|
13
23
|
import { getRandomNewLabelColor } from '../utils/taskConstants';
|
|
14
24
|
|
|
15
25
|
type WorkspaceTaskLabel = Pick<
|
|
@@ -68,10 +78,14 @@ export function useTaskLabelManagement({
|
|
|
68
78
|
|
|
69
79
|
// CRITICAL: Get current task state from cache instead of stale prop
|
|
70
80
|
// This ensures we read the most up-to-date state after optimistic updates
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
const canonicalTaskId = taskId ?? task.id;
|
|
82
|
+
const currentTask =
|
|
83
|
+
getTaskFromVisibleCaches({
|
|
84
|
+
queryClient,
|
|
85
|
+
boardId,
|
|
86
|
+
taskId: canonicalTaskId,
|
|
87
|
+
fallback: task,
|
|
88
|
+
}) ?? task;
|
|
75
89
|
|
|
76
90
|
// Check if we're in multi-select mode with multiple tasks selected
|
|
77
91
|
const shouldBulkUpdate =
|
|
@@ -86,11 +100,17 @@ export function useTaskLabelManagement({
|
|
|
86
100
|
|
|
87
101
|
// Cancel any outgoing refetches
|
|
88
102
|
await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
|
|
103
|
+
await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
|
|
89
104
|
|
|
90
105
|
// Snapshot the previous value BEFORE optimistic update
|
|
91
106
|
const previousTasks = queryClient.getQueryData(['tasks', boardId]) as
|
|
92
107
|
| Task[]
|
|
93
108
|
| undefined;
|
|
109
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(
|
|
110
|
+
queryClient,
|
|
111
|
+
boardId,
|
|
112
|
+
tasksToUpdate
|
|
113
|
+
);
|
|
94
114
|
|
|
95
115
|
// Determine action: remove if ALL selected tasks have the label, add otherwise
|
|
96
116
|
// Use currentTask from cache, not stale task prop
|
|
@@ -112,6 +132,13 @@ export function useTaskLabelManagement({
|
|
|
112
132
|
const fromBoardCache = previousTasks?.find((ct) => ct.id === taskId);
|
|
113
133
|
if (fromBoardCache) return fromBoardCache;
|
|
114
134
|
|
|
135
|
+
const fromVisibleCaches = getTaskFromVisibleCaches({
|
|
136
|
+
queryClient,
|
|
137
|
+
boardId,
|
|
138
|
+
taskId,
|
|
139
|
+
});
|
|
140
|
+
if (fromVisibleCaches) return fromVisibleCaches;
|
|
141
|
+
|
|
115
142
|
// Fallback to individual task cache (for tasks not in board view)
|
|
116
143
|
if (taskId === currentTask.id) return currentTask;
|
|
117
144
|
|
|
@@ -135,63 +162,36 @@ export function useTaskLabelManagement({
|
|
|
135
162
|
|
|
136
163
|
// Get label details from workspace labels for optimistic update
|
|
137
164
|
const label = workspaceLabels.find((l) => l.id === labelId);
|
|
165
|
+
const fallbackLabel = label || {
|
|
166
|
+
id: labelId,
|
|
167
|
+
name: 'Unknown',
|
|
168
|
+
color: '#3b82f6',
|
|
169
|
+
created_at: new Date().toISOString(),
|
|
170
|
+
};
|
|
138
171
|
|
|
139
172
|
// Optimistically update the cache - only update tasks that actually change
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
name: 'Unknown',
|
|
158
|
-
color: '#3b82f6',
|
|
159
|
-
created_at: new Date().toISOString(),
|
|
160
|
-
},
|
|
161
|
-
],
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return t;
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// CRITICAL: Also update the individual task cache if taskId is provided
|
|
169
|
-
// This ensures the chip menu's task cache stays in sync with the board cache
|
|
170
|
-
if (taskId) {
|
|
171
|
-
queryClient.setQueryData(['task', taskId], (old: Task | undefined) => {
|
|
172
|
-
if (!old) return old;
|
|
173
|
-
if (active && tasksToRemoveFrom.includes(taskId)) {
|
|
174
|
-
// Remove the label
|
|
175
|
-
return {
|
|
176
|
-
...old,
|
|
177
|
-
labels: old.labels?.filter((l) => l.id !== labelId) || [],
|
|
178
|
-
};
|
|
179
|
-
} else if (!active && tasksNeedingLabel.includes(taskId)) {
|
|
180
|
-
// Add the label
|
|
173
|
+
for (const tid of active ? tasksToRemoveFrom : tasksNeedingLabel) {
|
|
174
|
+
patchTaskInVisibleCaches({
|
|
175
|
+
queryClient,
|
|
176
|
+
boardId,
|
|
177
|
+
taskId: tid,
|
|
178
|
+
updater: (cachedTask) => {
|
|
179
|
+
if (active) {
|
|
180
|
+
return {
|
|
181
|
+
...cachedTask,
|
|
182
|
+
labels: cachedTask.labels?.filter((l) => l.id !== labelId) || [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (cachedTask.labels?.some((l) => l.id === labelId)) {
|
|
187
|
+
return cachedTask;
|
|
188
|
+
}
|
|
189
|
+
|
|
181
190
|
return {
|
|
182
|
-
...
|
|
183
|
-
labels: [
|
|
184
|
-
...(old.labels || []),
|
|
185
|
-
label || {
|
|
186
|
-
id: labelId,
|
|
187
|
-
name: 'Unknown',
|
|
188
|
-
color: '#3b82f6',
|
|
189
|
-
created_at: new Date().toISOString(),
|
|
190
|
-
},
|
|
191
|
-
],
|
|
191
|
+
...cachedTask,
|
|
192
|
+
labels: [...(cachedTask.labels || []), fallbackLabel],
|
|
192
193
|
};
|
|
193
|
-
}
|
|
194
|
-
return old;
|
|
194
|
+
},
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
|
|
@@ -201,33 +201,36 @@ export function useTaskLabelManagement({
|
|
|
201
201
|
? { baseUrl: window.location.origin }
|
|
202
202
|
: undefined;
|
|
203
203
|
let successCount = 0;
|
|
204
|
+
const succeededTaskIds: string[] = [];
|
|
204
205
|
|
|
205
206
|
if (active) {
|
|
206
|
-
for (const
|
|
207
|
+
for (const tid of tasksToRemoveFrom) {
|
|
207
208
|
try {
|
|
208
209
|
await removeWorkspaceTaskLabel(
|
|
209
210
|
workspaceId,
|
|
210
|
-
|
|
211
|
+
tid,
|
|
211
212
|
labelId,
|
|
212
213
|
internalApiOptions
|
|
213
214
|
);
|
|
214
215
|
successCount++;
|
|
216
|
+
succeededTaskIds.push(tid);
|
|
215
217
|
} catch (error) {
|
|
216
|
-
console.error(`Failed to remove label from task ${
|
|
218
|
+
console.error(`Failed to remove label from task ${tid}:`, error);
|
|
217
219
|
}
|
|
218
220
|
}
|
|
219
221
|
} else {
|
|
220
|
-
for (const
|
|
222
|
+
for (const tid of tasksNeedingLabel) {
|
|
221
223
|
try {
|
|
222
224
|
await addWorkspaceTaskLabel(
|
|
223
225
|
workspaceId,
|
|
224
|
-
|
|
226
|
+
tid,
|
|
225
227
|
labelId,
|
|
226
228
|
internalApiOptions
|
|
227
229
|
);
|
|
228
230
|
successCount++;
|
|
231
|
+
succeededTaskIds.push(tid);
|
|
229
232
|
} catch (error) {
|
|
230
|
-
console.error(`Failed to add label to task ${
|
|
233
|
+
console.error(`Failed to add label to task ${tid}:`, error);
|
|
231
234
|
}
|
|
232
235
|
}
|
|
233
236
|
}
|
|
@@ -240,10 +243,22 @@ export function useTaskLabelManagement({
|
|
|
240
243
|
throw new Error('Failed to update any tasks');
|
|
241
244
|
}
|
|
242
245
|
|
|
246
|
+
const failedTaskIds = (
|
|
247
|
+
active ? tasksToRemoveFrom : tasksNeedingLabel
|
|
248
|
+
).filter((tid) => !succeededTaskIds.includes(tid));
|
|
249
|
+
restoreTasksFromVisibleCacheSnapshot({
|
|
250
|
+
queryClient,
|
|
251
|
+
snapshot: cacheSnapshot,
|
|
252
|
+
taskIds: failedTaskIds,
|
|
253
|
+
});
|
|
254
|
+
|
|
243
255
|
// Broadcast relation changes for all affected tasks
|
|
244
|
-
for (const tid of
|
|
256
|
+
for (const tid of succeededTaskIds) {
|
|
245
257
|
broadcast?.('task:relations-changed', { taskId: tid });
|
|
246
258
|
}
|
|
259
|
+
if (succeededTaskIds.length > 0) {
|
|
260
|
+
getActiveBoardRefresh()?.({ invalidateTasks: false });
|
|
261
|
+
}
|
|
247
262
|
|
|
248
263
|
toast.success(active ? 'Label removed' : 'Label added', {
|
|
249
264
|
description:
|
|
@@ -253,9 +268,7 @@ export function useTaskLabelManagement({
|
|
|
253
268
|
// Don't auto-clear selection - let user manually clear with "Clear" button
|
|
254
269
|
} catch (e: any) {
|
|
255
270
|
// Rollback on error
|
|
256
|
-
|
|
257
|
-
queryClient.setQueryData(['tasks', boardId], previousTasks);
|
|
258
|
-
}
|
|
271
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
259
272
|
console.error('Failed to toggle label:', e);
|
|
260
273
|
toast.error('Error', {
|
|
261
274
|
description: 'Failed to update label. Please try again.',
|
|
@@ -306,48 +319,44 @@ export function useTaskLabelManagement({
|
|
|
306
319
|
|
|
307
320
|
// Auto-apply the newly created label to this task
|
|
308
321
|
let linkSucceeded = false;
|
|
309
|
-
|
|
322
|
+
const canonicalTaskId = taskId ?? task.id;
|
|
323
|
+
let cacheSnapshot:
|
|
324
|
+
| ReturnType<typeof snapshotVisibleTaskCaches>
|
|
325
|
+
| undefined;
|
|
310
326
|
try {
|
|
311
327
|
// Cancel any outgoing refetches
|
|
312
328
|
await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
|
|
329
|
+
await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
|
|
313
330
|
|
|
314
331
|
// Snapshot the previous value
|
|
315
|
-
|
|
332
|
+
cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
333
|
+
canonicalTaskId,
|
|
334
|
+
]);
|
|
316
335
|
|
|
317
336
|
// Optimistically update the cache
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
...t,
|
|
326
|
-
labels: [...(t.labels || []), newLabel],
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
return t;
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
// CRITICAL: Also update individual task cache if taskId is provided
|
|
335
|
-
if (taskId) {
|
|
336
|
-
queryClient.setQueryData(
|
|
337
|
-
['task', taskId],
|
|
338
|
-
(old: Task | undefined) => {
|
|
339
|
-
if (!old) return old;
|
|
340
|
-
return {
|
|
341
|
-
...old,
|
|
342
|
-
labels: [...(old.labels || []), newLabel],
|
|
343
|
-
};
|
|
337
|
+
patchTaskInVisibleCaches({
|
|
338
|
+
queryClient,
|
|
339
|
+
boardId,
|
|
340
|
+
taskId: canonicalTaskId,
|
|
341
|
+
updater: (cachedTask) => {
|
|
342
|
+
if (cachedTask.labels?.some((label) => label.id === newLabel.id)) {
|
|
343
|
+
return cachedTask;
|
|
344
344
|
}
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
...cachedTask,
|
|
348
|
+
labels: [...(cachedTask.labels || []), newLabel],
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
});
|
|
347
352
|
|
|
348
353
|
const taskState =
|
|
349
|
-
(
|
|
350
|
-
|
|
354
|
+
getTaskFromVisibleCaches({
|
|
355
|
+
queryClient,
|
|
356
|
+
boardId,
|
|
357
|
+
taskId: canonicalTaskId,
|
|
358
|
+
fallback: task,
|
|
359
|
+
}) ?? task;
|
|
351
360
|
const nextLabelIds = [
|
|
352
361
|
...new Set([
|
|
353
362
|
...(taskState.labels ?? []).map((entry) => entry.id),
|
|
@@ -357,7 +366,7 @@ export function useTaskLabelManagement({
|
|
|
357
366
|
|
|
358
367
|
await updateWorkspaceTask(
|
|
359
368
|
workspaceId,
|
|
360
|
-
|
|
369
|
+
canonicalTaskId,
|
|
361
370
|
{
|
|
362
371
|
label_ids: nextLabelIds,
|
|
363
372
|
},
|
|
@@ -368,19 +377,19 @@ export function useTaskLabelManagement({
|
|
|
368
377
|
linkSucceeded = true;
|
|
369
378
|
} catch (linkErr: any) {
|
|
370
379
|
// Rollback on error
|
|
371
|
-
|
|
380
|
+
if (cacheSnapshot) {
|
|
381
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
382
|
+
}
|
|
372
383
|
toast.error(
|
|
373
384
|
'The label was created but could not be attached to the task. Refresh and try manually.'
|
|
374
385
|
);
|
|
375
|
-
if (taskId) {
|
|
376
|
-
queryClient.invalidateQueries({ queryKey: ['task', taskId] });
|
|
377
|
-
}
|
|
378
386
|
console.error('Failed to auto-apply new label', linkErr);
|
|
379
387
|
}
|
|
380
388
|
|
|
381
389
|
// Only show success toast and reset form if link succeeded
|
|
382
390
|
if (linkSucceeded) {
|
|
383
|
-
broadcast?.('task:relations-changed', { taskId:
|
|
391
|
+
broadcast?.('task:relations-changed', { taskId: canonicalTaskId });
|
|
392
|
+
getActiveBoardRefresh()?.({ invalidateTasks: false });
|
|
384
393
|
|
|
385
394
|
// Reset form and close dialog
|
|
386
395
|
setNewLabelName('');
|