@tuturuuu/ui 0.5.0 → 0.6.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 +29 -0
- package/package.json +41 -34
- package/src/components/ui/currency-input.tsx +65 -23
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
- package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
- package/src/components/ui/custom/combobox.test.tsx +141 -0
- package/src/components/ui/custom/combobox.tsx +105 -36
- package/src/components/ui/custom/settings/task-settings.tsx +50 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +21 -1
- package/src/components/ui/custom/sidebar-context.tsx +68 -6
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
- package/src/components/ui/finance/finance-layout.tsx +2 -4
- package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
- package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
- package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
- package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
- package/src/components/ui/finance/transactions/form-types.ts +23 -0
- package/src/components/ui/finance/transactions/form.tsx +81 -22
- package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +73 -26
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +2 -1
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +4 -4
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +298 -34
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +219 -46
- package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
- package/src/components/ui/finance/wallets/columns.test.ts +56 -0
- package/src/components/ui/finance/wallets/columns.tsx +196 -43
- package/src/components/ui/finance/wallets/form.test.tsx +79 -14
- package/src/components/ui/finance/wallets/form.tsx +41 -197
- package/src/components/ui/finance/wallets/query-invalidation.ts +1 -0
- package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
- package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
- package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +64 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +42 -35
- package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
- package/src/components/ui/finance/wallets/wallets-page.test.tsx +111 -37
- package/src/components/ui/finance/wallets/wallets-page.tsx +38 -78
- package/src/components/ui/storefront/accent-button.tsx +33 -0
- package/src/components/ui/storefront/cart-summary.tsx +140 -0
- package/src/components/ui/storefront/empty-listings.tsx +32 -0
- package/src/components/ui/storefront/hero-panel.tsx +70 -0
- package/src/components/ui/storefront/image-panel.tsx +40 -0
- package/src/components/ui/storefront/index.ts +12 -0
- package/src/components/ui/storefront/listing-card.tsx +129 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
- package/src/components/ui/storefront/storefront-surface.tsx +235 -0
- package/src/components/ui/storefront/types.ts +99 -0
- package/src/components/ui/storefront/utils.ts +90 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
- package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +114 -7
- package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
- package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
- package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +128 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +104 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +17 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +151 -111
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +584 -53
- package/src/hooks/useBoardRealtime.ts +54 -1
- package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
- package/src/hooks/useTaskUserRealtime.ts +338 -0
|
@@ -144,6 +144,10 @@ import { areTaskCardPropsEqual } from './task-card-comparator';
|
|
|
144
144
|
import { shouldRenderTaskCardCompletionCheckbox } from './task-card-completion-checkbox-visibility';
|
|
145
145
|
import { TaskCardIdentifierRow } from './task-card-identifier-row';
|
|
146
146
|
import { mergeTaskCardLabelOptions } from './task-card-label-options';
|
|
147
|
+
import {
|
|
148
|
+
getTaskCardHydratingOpenOptions,
|
|
149
|
+
isExternalTaskSnapshot,
|
|
150
|
+
} from './task-card-open-options';
|
|
147
151
|
import { getTaskCardVisibilityState } from './task-card-visibility';
|
|
148
152
|
import { TaskSchedulingBadge } from './task-scheduling-badge';
|
|
149
153
|
|
|
@@ -662,8 +666,7 @@ function TaskCardInner({
|
|
|
662
666
|
|
|
663
667
|
// Check if task is optimistically added (pending realtime confirmation)
|
|
664
668
|
const isOptimistic = '_isOptimistic' in task && task._isOptimistic === true;
|
|
665
|
-
const isPersonalExternalTask =
|
|
666
|
-
task.is_personal_external === true || Boolean(task.personal_board_id);
|
|
669
|
+
const isPersonalExternalTask = isExternalTaskSnapshot(task);
|
|
667
670
|
const sourceBoardUrl =
|
|
668
671
|
task.source_workspace_id && task.source_board_id
|
|
669
672
|
? `/${task.source_workspace_id}${tasksHref(`/boards/${task.source_board_id}`)}`
|
|
@@ -887,51 +890,23 @@ function TaskCardInner({
|
|
|
887
890
|
// Removed explicit drag handle – entire card is now draggable for better UX.
|
|
888
891
|
// Keep attributes/listeners to spread onto root interactive area.
|
|
889
892
|
|
|
890
|
-
const openExternalTask = useCallback(
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
try {
|
|
903
|
-
sourceLists = (
|
|
904
|
-
await listWorkspaceTaskLists(sourceWorkspaceId, sourceBoardId)
|
|
905
|
-
).lists;
|
|
906
|
-
} catch {
|
|
907
|
-
sourceLists = undefined;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
openTask(sourceTask as Task, sourceBoardId, sourceLists, false, {
|
|
911
|
-
taskWsId: sourceWorkspaceId,
|
|
912
|
-
taskWorkspacePersonal: false,
|
|
913
|
-
});
|
|
914
|
-
return;
|
|
915
|
-
} catch {
|
|
916
|
-
const opened = await openTaskById(task.id);
|
|
917
|
-
if (opened) return;
|
|
918
|
-
}
|
|
919
|
-
} else {
|
|
920
|
-
const opened = await openTaskById(task.id);
|
|
921
|
-
if (opened) return;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
openTask(task, sourceBoardId ?? boardId, availableLists, false, {
|
|
925
|
-
taskWsId: sourceWorkspaceId ?? effectiveWorkspaceId,
|
|
926
|
-
taskWorkspacePersonal: sourceWorkspaceId ? false : isPersonalWorkspace,
|
|
927
|
-
});
|
|
893
|
+
const openExternalTask = useCallback(() => {
|
|
894
|
+
void openTaskById(
|
|
895
|
+
task.id,
|
|
896
|
+
getTaskCardHydratingOpenOptions({
|
|
897
|
+
task,
|
|
898
|
+
boardId,
|
|
899
|
+
availableLists,
|
|
900
|
+
effectiveWorkspaceId,
|
|
901
|
+
isPersonalWorkspace,
|
|
902
|
+
})
|
|
903
|
+
);
|
|
928
904
|
}, [
|
|
929
905
|
task,
|
|
930
906
|
boardId,
|
|
931
907
|
availableLists,
|
|
932
908
|
effectiveWorkspaceId,
|
|
933
909
|
isPersonalWorkspace,
|
|
934
|
-
openTask,
|
|
935
910
|
openTaskById,
|
|
936
911
|
]);
|
|
937
912
|
|
|
@@ -956,7 +931,7 @@ function TaskCardInner({
|
|
|
956
931
|
) {
|
|
957
932
|
// Only open edit dialog if not in multi-select mode, not dragging, and no other dialogs are open
|
|
958
933
|
if (isPersonalExternalTask) {
|
|
959
|
-
|
|
934
|
+
openExternalTask();
|
|
960
935
|
return;
|
|
961
936
|
}
|
|
962
937
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
4
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
5
|
+
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { TimelineBoard } from './timeline-board';
|
|
8
|
+
|
|
9
|
+
const createTaskMock = vi.hoisted(() => vi.fn());
|
|
10
|
+
const openTaskMock = vi.hoisted(() => vi.fn());
|
|
11
|
+
const openTaskByIdMock = vi.hoisted(() => vi.fn());
|
|
12
|
+
const updateWorkspaceTaskMock = vi.hoisted(() => vi.fn());
|
|
13
|
+
const deleteWorkspaceTaskMock = vi.hoisted(() => vi.fn());
|
|
14
|
+
|
|
15
|
+
class MockResizeObserver {
|
|
16
|
+
observe() {}
|
|
17
|
+
unobserve() {}
|
|
18
|
+
disconnect() {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
globalThis.ResizeObserver = MockResizeObserver as typeof ResizeObserver;
|
|
22
|
+
|
|
23
|
+
vi.mock('next-intl', () => ({
|
|
24
|
+
useLocale: () => 'en',
|
|
25
|
+
useTranslations: () => (key: string) => key,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('@tuturuuu/internal-api', () => ({
|
|
29
|
+
updateWorkspaceTask: updateWorkspaceTaskMock,
|
|
30
|
+
deleteWorkspaceTask: deleteWorkspaceTaskMock,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock('../../hooks/useTaskDialog', () => ({
|
|
34
|
+
useTaskDialog: () => ({
|
|
35
|
+
createTask: createTaskMock,
|
|
36
|
+
openTask: openTaskMock,
|
|
37
|
+
openTaskById: openTaskByIdMock,
|
|
38
|
+
}),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
vi.mock('./timeline/timeline-grid', () => ({
|
|
42
|
+
TimelineGrid: ({
|
|
43
|
+
localTasks,
|
|
44
|
+
onOpenTask,
|
|
45
|
+
}: {
|
|
46
|
+
localTasks: Task[];
|
|
47
|
+
onOpenTask: (task: Task) => void;
|
|
48
|
+
}) => (
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
data-testid="open-timeline-task"
|
|
52
|
+
onClick={() => onOpenTask(localTasks[0]!)}
|
|
53
|
+
>
|
|
54
|
+
Open timeline task
|
|
55
|
+
</button>
|
|
56
|
+
),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
const lists: TaskList[] = [
|
|
60
|
+
{
|
|
61
|
+
archived: false,
|
|
62
|
+
board_id: 'board-1',
|
|
63
|
+
color: 'GRAY',
|
|
64
|
+
created_at: '2026-05-01T00:00:00.000Z',
|
|
65
|
+
creator_id: 'user-1',
|
|
66
|
+
deleted: false,
|
|
67
|
+
id: 'todo',
|
|
68
|
+
name: 'To Do',
|
|
69
|
+
position: 0,
|
|
70
|
+
status: 'not_started',
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
function task(overrides: Partial<Task> & Pick<Task, 'id' | 'name'>): Task {
|
|
75
|
+
return {
|
|
76
|
+
created_at: '2026-05-01T00:00:00.000Z',
|
|
77
|
+
display_number: 1,
|
|
78
|
+
end_date: null,
|
|
79
|
+
labels: [],
|
|
80
|
+
list_id: 'todo',
|
|
81
|
+
priority: 'normal',
|
|
82
|
+
sort_key: 1,
|
|
83
|
+
start_date: null,
|
|
84
|
+
...overrides,
|
|
85
|
+
} as Task;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderTimeline(tasks: Task[]) {
|
|
89
|
+
const queryClient = new QueryClient({
|
|
90
|
+
defaultOptions: {
|
|
91
|
+
mutations: { retry: false },
|
|
92
|
+
queries: { retry: false },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return render(
|
|
97
|
+
<QueryClientProvider client={queryClient}>
|
|
98
|
+
<TimelineBoard
|
|
99
|
+
boardId="board-1"
|
|
100
|
+
lists={lists}
|
|
101
|
+
tasks={tasks}
|
|
102
|
+
wsId="ws-1"
|
|
103
|
+
/>
|
|
104
|
+
</QueryClientProvider>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe('TimelineBoard task opening', () => {
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
createTaskMock.mockReset();
|
|
111
|
+
openTaskMock.mockReset();
|
|
112
|
+
openTaskByIdMock.mockReset();
|
|
113
|
+
updateWorkspaceTaskMock.mockReset();
|
|
114
|
+
deleteWorkspaceTaskMock.mockReset();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('opens external source tasks through the hydrating task-by-id path immediately', () => {
|
|
118
|
+
renderTimeline([
|
|
119
|
+
task({
|
|
120
|
+
id: 'external-timeline-task',
|
|
121
|
+
name: 'External timeline task',
|
|
122
|
+
list_id: 'personal-list',
|
|
123
|
+
personal_board_id: 'board-1',
|
|
124
|
+
is_personal_external: true,
|
|
125
|
+
source_workspace_id: 'source-ws',
|
|
126
|
+
source_board_id: 'source-board',
|
|
127
|
+
source_board_name: 'Source board',
|
|
128
|
+
source_list_id: 'source-list',
|
|
129
|
+
source_list_name: 'Source list',
|
|
130
|
+
}),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
fireEvent.click(screen.getByTestId('open-timeline-task'));
|
|
134
|
+
|
|
135
|
+
expect(openTaskByIdMock).toHaveBeenCalledWith(
|
|
136
|
+
'external-timeline-task',
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
boardId: 'source-board',
|
|
139
|
+
taskWsId: 'source-ws',
|
|
140
|
+
taskWorkspacePersonal: false,
|
|
141
|
+
initialTask: expect.objectContaining({
|
|
142
|
+
id: 'external-timeline-task',
|
|
143
|
+
list_id: 'source-list',
|
|
144
|
+
name: 'External timeline task',
|
|
145
|
+
}),
|
|
146
|
+
initialSharedContext: expect.objectContaining({
|
|
147
|
+
boardConfig: expect.objectContaining({
|
|
148
|
+
id: 'source-board',
|
|
149
|
+
name: 'Source board',
|
|
150
|
+
ws_id: 'source-ws',
|
|
151
|
+
}),
|
|
152
|
+
availableLists: [
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
id: 'source-list',
|
|
155
|
+
name: 'Source list',
|
|
156
|
+
board_id: 'source-board',
|
|
157
|
+
}),
|
|
158
|
+
],
|
|
159
|
+
}),
|
|
160
|
+
})
|
|
161
|
+
);
|
|
162
|
+
expect(openTaskMock).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -32,6 +32,10 @@ import {
|
|
|
32
32
|
useState,
|
|
33
33
|
} from 'react';
|
|
34
34
|
import { useTaskDialog } from '../../hooks/useTaskDialog';
|
|
35
|
+
import {
|
|
36
|
+
getTaskCardHydratingOpenOptions,
|
|
37
|
+
isExternalTaskSnapshot,
|
|
38
|
+
} from './task-card/task-card-open-options';
|
|
35
39
|
import { TaskEditDialog } from './timeline/task-edit-dialog';
|
|
36
40
|
import {
|
|
37
41
|
DEFAULT_DAY_WIDTH,
|
|
@@ -111,7 +115,7 @@ export function TimelineBoard({
|
|
|
111
115
|
}: TimelineProps) {
|
|
112
116
|
const t = useTranslations('common');
|
|
113
117
|
const locale = useLocale();
|
|
114
|
-
const { createTask, openTask } = useTaskDialog();
|
|
118
|
+
const { createTask, openTask, openTaskById } = useTaskDialog();
|
|
115
119
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
116
120
|
const [dayWidth, setDayWidth] = useState(DEFAULT_DAY_WIDTH);
|
|
117
121
|
const [density, setDensity] = useState<Density>('comfortable');
|
|
@@ -226,23 +230,28 @@ export function TimelineBoard({
|
|
|
226
230
|
|
|
227
231
|
const openTimelineTask = useCallback(
|
|
228
232
|
(task: Task) => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
if (!boardId) return;
|
|
234
|
+
|
|
235
|
+
if (isExternalTaskSnapshot(task)) {
|
|
236
|
+
void openTaskById(
|
|
237
|
+
task.id,
|
|
238
|
+
getTaskCardHydratingOpenOptions({
|
|
239
|
+
task,
|
|
240
|
+
boardId,
|
|
241
|
+
availableLists: lists,
|
|
242
|
+
effectiveWorkspaceId: wsId,
|
|
243
|
+
isPersonalWorkspace: Boolean(wsId),
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
233
248
|
|
|
234
|
-
openTask(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
false,
|
|
239
|
-
{
|
|
240
|
-
taskWsId: targetWorkspaceId,
|
|
241
|
-
taskWorkspacePersonal: Boolean(wsId) && !task.source_workspace_id,
|
|
242
|
-
}
|
|
243
|
-
);
|
|
249
|
+
openTask(task, boardId, lists, false, {
|
|
250
|
+
taskWsId: wsId,
|
|
251
|
+
taskWorkspacePersonal: Boolean(wsId),
|
|
252
|
+
});
|
|
244
253
|
},
|
|
245
|
-
[boardId, lists, openTask, wsId]
|
|
254
|
+
[boardId, lists, openTask, openTaskById, wsId]
|
|
246
255
|
);
|
|
247
256
|
|
|
248
257
|
const clearDraft = useCallback((taskId: string) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkspaceProductTier } from '@tuturuuu/types';
|
|
1
2
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
3
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
3
4
|
import type { TaskFilters } from '@tuturuuu/ui/tu-do/boards/boardId/task-filter';
|
|
@@ -5,6 +6,7 @@ import {
|
|
|
5
6
|
type PendingRelationshipType,
|
|
6
7
|
useTaskDialogContext,
|
|
7
8
|
} from '../providers/task-dialog-provider';
|
|
9
|
+
import type { SharedTaskContext } from '../shared/task-edit-dialog/hooks/use-task-data';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Hook to open and manage the centralized task dialog
|
|
@@ -50,7 +52,19 @@ export function useTaskDialog(): {
|
|
|
50
52
|
taskWorkspacePersonal?: boolean;
|
|
51
53
|
}
|
|
52
54
|
) => void;
|
|
53
|
-
openTaskById: (
|
|
55
|
+
openTaskById: (
|
|
56
|
+
taskId: string,
|
|
57
|
+
options?: {
|
|
58
|
+
initialTask?: Partial<Task>;
|
|
59
|
+
boardId?: string;
|
|
60
|
+
availableLists?: TaskList[];
|
|
61
|
+
fakeTaskUrl?: boolean;
|
|
62
|
+
taskWsId?: string;
|
|
63
|
+
taskWorkspacePersonal?: boolean;
|
|
64
|
+
taskWorkspaceTier?: WorkspaceProductTier;
|
|
65
|
+
initialSharedContext?: SharedTaskContext;
|
|
66
|
+
}
|
|
67
|
+
) => Promise<boolean>;
|
|
54
68
|
createTask: (
|
|
55
69
|
boardId: string,
|
|
56
70
|
listId: string,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '@tuturuuu/internal-api';
|
|
17
17
|
import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
|
|
18
18
|
import { AI_CREDITS_QUERY_KEY } from '@tuturuuu/ui/hooks/use-ai-credits';
|
|
19
|
+
import { useTaskUserRealtime } from '@tuturuuu/ui/hooks/useTaskUserRealtime';
|
|
19
20
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
20
21
|
import { useTaskDialog } from '@tuturuuu/ui/tu-do/hooks/useTaskDialog';
|
|
21
22
|
import { useBoardConfig } from '@tuturuuu/utils/task-helper';
|
|
@@ -51,6 +52,7 @@ export function useMyTasksState({
|
|
|
51
52
|
const t = useTranslations();
|
|
52
53
|
const queryClient = useQueryClient();
|
|
53
54
|
const { onUpdate, openTaskById } = useTaskDialog();
|
|
55
|
+
useTaskUserRealtime(userId);
|
|
54
56
|
|
|
55
57
|
// Filter state (declared before query so it can be passed as param)
|
|
56
58
|
const [taskFilters, setTaskFilters] = useState<{
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '@tuturuuu/internal-api';
|
|
11
11
|
import type { TaskWithRelations } from '@tuturuuu/types';
|
|
12
12
|
import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
|
|
13
|
+
import { getActiveTaskUserBroadcast } from '@tuturuuu/ui/hooks/useTaskUserRealtime';
|
|
13
14
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
14
15
|
import { useTranslations } from 'next-intl';
|
|
15
16
|
import { useCallback, useState } from 'react';
|
|
@@ -38,6 +39,7 @@ export function useTaskContextActions({
|
|
|
38
39
|
const [isLoading, setIsLoading] = useState(false);
|
|
39
40
|
const taskWorkspaceId = task.list?.board?.ws_id ?? null;
|
|
40
41
|
const taskBoardId = task.list?.board?.id ?? null;
|
|
42
|
+
const taskListId = task.list_id ?? task.list?.id ?? null;
|
|
41
43
|
|
|
42
44
|
const invalidateQueries = useCallback(() => {
|
|
43
45
|
queryClient.invalidateQueries({ queryKey: [MY_TASKS_QUERY_KEY] });
|
|
@@ -66,6 +68,63 @@ export function useTaskContextActions({
|
|
|
66
68
|
[queryClient, task.id]
|
|
67
69
|
);
|
|
68
70
|
|
|
71
|
+
const broadcastTaskUpsert = useCallback(
|
|
72
|
+
(updates: Record<string, unknown>) => {
|
|
73
|
+
const broadcast = getActiveTaskUserBroadcast();
|
|
74
|
+
if (!broadcast) return;
|
|
75
|
+
|
|
76
|
+
const nextListId =
|
|
77
|
+
typeof updates.list_id === 'string' ? updates.list_id : taskListId;
|
|
78
|
+
broadcast('task:upsert', {
|
|
79
|
+
actor_user_id: userId,
|
|
80
|
+
actorUserId: userId,
|
|
81
|
+
boardId: taskBoardId,
|
|
82
|
+
listId: nextListId,
|
|
83
|
+
task: {
|
|
84
|
+
id: task.id,
|
|
85
|
+
name: task.name,
|
|
86
|
+
description: task.description ?? null,
|
|
87
|
+
priority: task.priority ?? null,
|
|
88
|
+
start_date: task.start_date ?? null,
|
|
89
|
+
end_date: task.end_date ?? null,
|
|
90
|
+
list_id: nextListId,
|
|
91
|
+
created_at: task.created_at ?? null,
|
|
92
|
+
list: task.list,
|
|
93
|
+
overrides: task.overrides,
|
|
94
|
+
...updates,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
[task, taskBoardId, taskListId, userId]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const broadcastTaskDelete = useCallback(() => {
|
|
102
|
+
const broadcast = getActiveTaskUserBroadcast();
|
|
103
|
+
if (!broadcast) return;
|
|
104
|
+
|
|
105
|
+
broadcast('task:delete', {
|
|
106
|
+
actor_user_id: userId,
|
|
107
|
+
actorUserId: userId,
|
|
108
|
+
boardId: taskBoardId,
|
|
109
|
+
listId: taskListId,
|
|
110
|
+
taskId: task.id,
|
|
111
|
+
});
|
|
112
|
+
}, [task.id, taskBoardId, taskListId, userId]);
|
|
113
|
+
|
|
114
|
+
const broadcastTaskRelationsChanged = useCallback(() => {
|
|
115
|
+
const broadcast = getActiveTaskUserBroadcast();
|
|
116
|
+
if (!broadcast) return;
|
|
117
|
+
|
|
118
|
+
broadcast('task:relations-changed', {
|
|
119
|
+
actor_user_id: userId,
|
|
120
|
+
actorUserId: userId,
|
|
121
|
+
boardId: taskBoardId,
|
|
122
|
+
listId: taskListId,
|
|
123
|
+
taskId: task.id,
|
|
124
|
+
taskIds: [task.id],
|
|
125
|
+
});
|
|
126
|
+
}, [task.id, taskBoardId, taskListId, userId]);
|
|
127
|
+
|
|
69
128
|
const handlePriorityChange = useCallback(
|
|
70
129
|
async (priority: TaskPriority | null) => {
|
|
71
130
|
setIsLoading(true);
|
|
@@ -73,6 +132,7 @@ export function useTaskContextActions({
|
|
|
73
132
|
try {
|
|
74
133
|
if (!taskWorkspaceId) throw new Error('Task workspace not found');
|
|
75
134
|
await updateWorkspaceTask(taskWorkspaceId, task.id, { priority });
|
|
135
|
+
broadcastTaskUpsert({ priority });
|
|
76
136
|
invalidateQueries();
|
|
77
137
|
dispatchTaskSoundCue('update');
|
|
78
138
|
} catch {
|
|
@@ -82,7 +142,14 @@ export function useTaskContextActions({
|
|
|
82
142
|
setIsLoading(false);
|
|
83
143
|
}
|
|
84
144
|
},
|
|
85
|
-
[
|
|
145
|
+
[
|
|
146
|
+
task.id,
|
|
147
|
+
taskWorkspaceId,
|
|
148
|
+
updateTaskInCache,
|
|
149
|
+
invalidateQueries,
|
|
150
|
+
broadcastTaskUpsert,
|
|
151
|
+
t,
|
|
152
|
+
]
|
|
86
153
|
);
|
|
87
154
|
|
|
88
155
|
const handleDueDateChange = useCallback(
|
|
@@ -98,6 +165,7 @@ export function useTaskContextActions({
|
|
|
98
165
|
await updateWorkspaceTask(taskWorkspaceId, task.id, {
|
|
99
166
|
end_date: newDate,
|
|
100
167
|
});
|
|
168
|
+
broadcastTaskUpsert({ end_date: newDate });
|
|
101
169
|
invalidateQueries();
|
|
102
170
|
dispatchTaskSoundCue('update');
|
|
103
171
|
} catch {
|
|
@@ -107,7 +175,14 @@ export function useTaskContextActions({
|
|
|
107
175
|
setIsLoading(false);
|
|
108
176
|
}
|
|
109
177
|
},
|
|
110
|
-
[
|
|
178
|
+
[
|
|
179
|
+
task.id,
|
|
180
|
+
taskWorkspaceId,
|
|
181
|
+
updateTaskInCache,
|
|
182
|
+
invalidateQueries,
|
|
183
|
+
broadcastTaskUpsert,
|
|
184
|
+
t,
|
|
185
|
+
]
|
|
111
186
|
);
|
|
112
187
|
|
|
113
188
|
const handleToggleLabel = useCallback(
|
|
@@ -121,6 +196,7 @@ export function useTaskContextActions({
|
|
|
121
196
|
} else {
|
|
122
197
|
await addWorkspaceTaskLabel(taskWorkspaceId, task.id, labelId);
|
|
123
198
|
}
|
|
199
|
+
broadcastTaskRelationsChanged();
|
|
124
200
|
invalidateQueries();
|
|
125
201
|
dispatchTaskSoundCue('update');
|
|
126
202
|
} catch {
|
|
@@ -130,7 +206,14 @@ export function useTaskContextActions({
|
|
|
130
206
|
setIsLoading(false);
|
|
131
207
|
}
|
|
132
208
|
},
|
|
133
|
-
[
|
|
209
|
+
[
|
|
210
|
+
task.id,
|
|
211
|
+
task.labels,
|
|
212
|
+
taskWorkspaceId,
|
|
213
|
+
invalidateQueries,
|
|
214
|
+
broadcastTaskRelationsChanged,
|
|
215
|
+
t,
|
|
216
|
+
]
|
|
134
217
|
);
|
|
135
218
|
|
|
136
219
|
const handleComplete = useCallback(async () => {
|
|
@@ -151,6 +234,9 @@ export function useTaskContextActions({
|
|
|
151
234
|
await updateWorkspaceTask(taskWorkspaceId, task.id, {
|
|
152
235
|
list_id: doneList.id,
|
|
153
236
|
});
|
|
237
|
+
broadcastTaskUpsert({
|
|
238
|
+
list_id: doneList.id,
|
|
239
|
+
});
|
|
154
240
|
|
|
155
241
|
// Clear redundant personal overrides when task is actually done
|
|
156
242
|
if (
|
|
@@ -182,6 +268,7 @@ export function useTaskContextActions({
|
|
|
182
268
|
taskBoardId,
|
|
183
269
|
taskWorkspaceId,
|
|
184
270
|
task.overrides,
|
|
271
|
+
broadcastTaskUpsert,
|
|
185
272
|
onTaskUpdate,
|
|
186
273
|
onClose,
|
|
187
274
|
t,
|
|
@@ -199,6 +286,7 @@ export function useTaskContextActions({
|
|
|
199
286
|
}
|
|
200
287
|
);
|
|
201
288
|
if (!response.ok) throw new Error('Failed');
|
|
289
|
+
broadcastTaskDelete();
|
|
202
290
|
onTaskUpdate();
|
|
203
291
|
onClose();
|
|
204
292
|
dispatchTaskSoundCue('complete');
|
|
@@ -207,7 +295,7 @@ export function useTaskContextActions({
|
|
|
207
295
|
} finally {
|
|
208
296
|
setIsLoading(false);
|
|
209
297
|
}
|
|
210
|
-
}, [task.id, onTaskUpdate, onClose, t]);
|
|
298
|
+
}, [task.id, broadcastTaskDelete, onTaskUpdate, onClose, t]);
|
|
211
299
|
|
|
212
300
|
const handleUndoDoneWithMyPart = useCallback(async () => {
|
|
213
301
|
setIsLoading(true);
|
|
@@ -224,6 +312,13 @@ export function useTaskContextActions({
|
|
|
224
312
|
}
|
|
225
313
|
);
|
|
226
314
|
if (!response.ok) throw new Error('Failed');
|
|
315
|
+
broadcastTaskUpsert({
|
|
316
|
+
overrides: {
|
|
317
|
+
...task.overrides,
|
|
318
|
+
personally_unassigned: false,
|
|
319
|
+
completed_at: null,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
227
322
|
onTaskUpdate();
|
|
228
323
|
onClose();
|
|
229
324
|
dispatchTaskSoundCue('update');
|
|
@@ -232,7 +327,7 @@ export function useTaskContextActions({
|
|
|
232
327
|
} finally {
|
|
233
328
|
setIsLoading(false);
|
|
234
329
|
}
|
|
235
|
-
}, [task.id, onTaskUpdate, onClose, t]);
|
|
330
|
+
}, [task.id, task.overrides, broadcastTaskUpsert, onTaskUpdate, onClose, t]);
|
|
236
331
|
|
|
237
332
|
const handleUndoComplete = useCallback(async () => {
|
|
238
333
|
if (!taskBoardId || !taskWorkspaceId) return;
|
|
@@ -254,6 +349,7 @@ export function useTaskContextActions({
|
|
|
254
349
|
await updateWorkspaceTask(taskWorkspaceId, task.id, {
|
|
255
350
|
list_id: activeList.id,
|
|
256
351
|
});
|
|
352
|
+
broadcastTaskUpsert({ list_id: activeList.id });
|
|
257
353
|
|
|
258
354
|
onTaskUpdate();
|
|
259
355
|
onClose();
|
|
@@ -263,7 +359,15 @@ export function useTaskContextActions({
|
|
|
263
359
|
} finally {
|
|
264
360
|
setIsLoading(false);
|
|
265
361
|
}
|
|
266
|
-
}, [
|
|
362
|
+
}, [
|
|
363
|
+
task.id,
|
|
364
|
+
taskBoardId,
|
|
365
|
+
taskWorkspaceId,
|
|
366
|
+
broadcastTaskUpsert,
|
|
367
|
+
onTaskUpdate,
|
|
368
|
+
onClose,
|
|
369
|
+
t,
|
|
370
|
+
]);
|
|
267
371
|
|
|
268
372
|
const handleUnassignMe = useCallback(async () => {
|
|
269
373
|
setIsLoading(true);
|
|
@@ -291,6 +395,7 @@ export function useTaskContextActions({
|
|
|
291
395
|
Boolean(assigneeId && assigneeId !== userId)
|
|
292
396
|
),
|
|
293
397
|
});
|
|
398
|
+
broadcastTaskDelete();
|
|
294
399
|
onTaskUpdate();
|
|
295
400
|
onClose();
|
|
296
401
|
dispatchTaskSoundCue('update');
|
|
@@ -310,6 +415,7 @@ export function useTaskContextActions({
|
|
|
310
415
|
invalidateQueries,
|
|
311
416
|
t,
|
|
312
417
|
task.assignees,
|
|
418
|
+
broadcastTaskDelete,
|
|
313
419
|
]);
|
|
314
420
|
|
|
315
421
|
const handleDelete = useCallback(async () => {
|
|
@@ -317,6 +423,7 @@ export function useTaskContextActions({
|
|
|
317
423
|
try {
|
|
318
424
|
if (!taskWorkspaceId) throw new Error('Task workspace not found');
|
|
319
425
|
await deleteWorkspaceTask(taskWorkspaceId, task.id);
|
|
426
|
+
broadcastTaskDelete();
|
|
320
427
|
onTaskUpdate();
|
|
321
428
|
onClose();
|
|
322
429
|
dispatchTaskSoundCue('delete');
|
|
@@ -325,7 +432,7 @@ export function useTaskContextActions({
|
|
|
325
432
|
} finally {
|
|
326
433
|
setIsLoading(false);
|
|
327
434
|
}
|
|
328
|
-
}, [task.id, taskWorkspaceId, onTaskUpdate, onClose, t]);
|
|
435
|
+
}, [task.id, taskWorkspaceId, broadcastTaskDelete, onTaskUpdate, onClose, t]);
|
|
329
436
|
|
|
330
437
|
return {
|
|
331
438
|
isLoading,
|