@tuturuuu/ui 0.12.0 → 0.13.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 +16 -0
- package/package.json +4 -4
- package/src/components/ui/calendar-app/components/habits-panel.tsx +5 -2
- package/src/components/ui/calendar-app/components/priority-view.tsx +15 -7
- package/src/components/ui/calendar-app/components/scheduling-dialog.tsx +7 -3
- package/src/components/ui/calendar-app/components/task-scheduler-panel.tsx +7 -3
- package/src/components/ui/calendar-app/components/time-tracker/index.tsx +14 -9
- package/src/components/ui/custom/settings/task-settings.tsx +6 -2
- package/src/components/ui/custom/tables/data-table.tsx +58 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +3 -6
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +11 -1
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -3
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-resource-context.test.ts +113 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-resource-context.ts +50 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +65 -18
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +1 -1
- package/src/components/ui/tu-do/cycles/task-cycles-client.tsx +23 -14
- package/src/components/ui/tu-do/estimates/use-task-estimates.ts +9 -4
- package/src/components/ui/tu-do/habits/client.tsx +4 -1
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +6 -3
- package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +19 -11
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-query.ts +15 -6
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +18 -13
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +17 -10
- package/src/components/ui/tu-do/shared/fade-setting-initializer.tsx +3 -1
- package/src/components/ui/tu-do/shared/list-view.tsx +1 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +6 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-update-shared-task.test.ts +9 -5
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +13 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-overrides.ts +21 -9
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-update-shared-task.ts +10 -5
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +4 -2
- package/src/components/ui/tu-do/shared/task-estimation-picker.tsx +5 -2
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +1 -1
- package/src/hooks/__tests__/use-task-actions.test.tsx +51 -0
- package/src/hooks/task-actions-personal-external.ts +2 -3
- package/src/hooks/use-board-actions.ts +27 -17
- package/src/hooks/use-calendar.tsx +7 -3
- package/src/hooks/use-settings-dialog-shortcut.ts +51 -0
- package/src/lib/task-personal-external.ts +49 -0
- package/src/lib/tasks-app-url.ts +85 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.0](https://github.com/tutur3u/platform/compare/ui-v0.12.0...ui-v0.13.0) (2026-07-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **satellite:** open settings on Cmd/Ctrl+, in satellite apps ([841f425](https://github.com/tutur3u/platform/commit/841f42557dd49eb31fd797900b1ec6ade6642dba))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **tasks:** harden satellite task route hydration ([9d51a38](https://github.com/tutur3u/platform/commit/9d51a38ad948ebb398bec794f082f2bbf8d466cc))
|
|
14
|
+
* **tasks:** keep personal board moves on workspace route ([60e29b0](https://github.com/tutur3u/platform/commit/60e29b093314b3b185f3f63a69042a665644ed3d))
|
|
15
|
+
* **tasks:** keep personal moves off placement API ([d8c41d7](https://github.com/tutur3u/platform/commit/d8c41d790f5edbe637b9ce71bbd5040b7fddbfa5))
|
|
16
|
+
* **tasks:** restore satellite resource access ([ec5e0a8](https://github.com/tutur3u/platform/commit/ec5e0a8d75398047e0151ae2ac3cd9a25a4fd675))
|
|
17
|
+
* **tasks:** route native personal task moves normally ([5bbba3c](https://github.com/tutur3u/platform/commit/5bbba3c3f193e722cc87cdfe2eb2734f61442cd0))
|
|
18
|
+
|
|
3
19
|
## [0.12.0](https://github.com/tutur3u/platform/compare/ui-v0.11.1...ui-v0.12.0) (2026-07-03)
|
|
4
20
|
|
|
5
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -85,12 +85,12 @@
|
|
|
85
85
|
"@tiptap/react": "3.27.1",
|
|
86
86
|
"@tiptap/starter-kit": "3.27.1",
|
|
87
87
|
"@tuturuuu/ai": "0.2.2",
|
|
88
|
-
"@tuturuuu/apis": "0.7.
|
|
88
|
+
"@tuturuuu/apis": "0.7.1",
|
|
89
89
|
"@tuturuuu/hooks": "0.0.2",
|
|
90
90
|
"@tuturuuu/icons": "0.0.6",
|
|
91
|
-
"@tuturuuu/internal-api": "0.14.
|
|
91
|
+
"@tuturuuu/internal-api": "0.14.1",
|
|
92
92
|
"@tuturuuu/supabase": "0.4.0",
|
|
93
|
-
"@tuturuuu/utils": "0.
|
|
93
|
+
"@tuturuuu/utils": "0.13.0",
|
|
94
94
|
"@types/debug": "^4.1.13",
|
|
95
95
|
"browser-image-compression": "^2.0.2",
|
|
96
96
|
"class-variance-authority": "^0.7.1",
|
|
@@ -15,6 +15,7 @@ import { getRecurrenceDescription } from '@tuturuuu/types/primitives/Habit';
|
|
|
15
15
|
import { cn } from '@tuturuuu/utils/format';
|
|
16
16
|
import Link from 'next/link';
|
|
17
17
|
import { useState } from 'react';
|
|
18
|
+
import { getTaskApiUrl, getTasksAppUrl } from '../../../../lib/tasks-app-url';
|
|
18
19
|
import { Badge } from '../../badge';
|
|
19
20
|
import { Button } from '../../button';
|
|
20
21
|
import { Card, CardContent } from '../../card';
|
|
@@ -65,7 +66,9 @@ export function HabitsPanel({ wsId }: HabitsPanelProps) {
|
|
|
65
66
|
} = useQuery({
|
|
66
67
|
queryKey: ['habits', wsId],
|
|
67
68
|
queryFn: async () => {
|
|
68
|
-
const response = await fetch(
|
|
69
|
+
const response = await fetch(
|
|
70
|
+
getTaskApiUrl(`/api/v1/workspaces/${wsId}/habits`)
|
|
71
|
+
);
|
|
69
72
|
if (!response.ok) {
|
|
70
73
|
throw new Error('Failed to fetch habits');
|
|
71
74
|
}
|
|
@@ -308,7 +311,7 @@ export function HabitsPanel({ wsId }: HabitsPanelProps) {
|
|
|
308
311
|
|
|
309
312
|
{/* Footer */}
|
|
310
313
|
<div className="border-t p-3">
|
|
311
|
-
<Link href={`/${wsId}/
|
|
314
|
+
<Link href={getTasksAppUrl(`/${wsId}/habits`)}>
|
|
312
315
|
<Button variant="outline" size="sm" className="w-full">
|
|
313
316
|
Manage Habits
|
|
314
317
|
</Button>
|
|
@@ -32,6 +32,7 @@ import { useRouter } from 'next/navigation';
|
|
|
32
32
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
33
33
|
import { useDebouncedCallback } from 'use-debounce';
|
|
34
34
|
import { useCalendar } from '../../../../hooks/use-calendar';
|
|
35
|
+
import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
|
|
35
36
|
import { Progress } from '../../progress';
|
|
36
37
|
import { toast } from '../../sonner';
|
|
37
38
|
import type { ExtendedWorkspaceTask } from '../../time-tracker/types';
|
|
@@ -263,9 +264,11 @@ export default function PriorityView({
|
|
|
263
264
|
const results = await Promise.allSettled(
|
|
264
265
|
tasksToFetchSchedule.map(async (task) => {
|
|
265
266
|
const response = await fetch(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
getTaskApiUrl(
|
|
268
|
+
isPersonalWorkspace
|
|
269
|
+
? `/api/v1/users/me/tasks/${task.id}/schedule`
|
|
270
|
+
: `/api/v1/workspaces/${task.ws_id ?? wsId}/tasks/${task.id}/schedule`
|
|
271
|
+
),
|
|
269
272
|
{ cache: 'no-store' }
|
|
270
273
|
);
|
|
271
274
|
if (!response.ok) {
|
|
@@ -378,10 +381,15 @@ export default function PriorityView({
|
|
|
378
381
|
const task = combinedTasks.find((t) => t.id === taskId);
|
|
379
382
|
const taskWsId = task?.ws_id ?? wsId;
|
|
380
383
|
|
|
381
|
-
const response = await fetch(
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
384
|
+
const response = await fetch(
|
|
385
|
+
getTaskApiUrl(`/api/${taskWsId}/task/${taskId}/edit`),
|
|
386
|
+
{
|
|
387
|
+
method: 'PATCH',
|
|
388
|
+
credentials: 'include',
|
|
389
|
+
headers: { 'Content-Type': 'application/json' },
|
|
390
|
+
body: JSON.stringify({ priority: newPriority }),
|
|
391
|
+
}
|
|
392
|
+
);
|
|
385
393
|
|
|
386
394
|
if (!response.ok) {
|
|
387
395
|
throw new Error('Failed to update task priority');
|
|
@@ -17,6 +17,7 @@ import { createClient } from '@tuturuuu/supabase/next/client';
|
|
|
17
17
|
import { cn } from '@tuturuuu/utils/format';
|
|
18
18
|
import { useRouter } from 'next/navigation';
|
|
19
19
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
20
|
+
import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
|
|
20
21
|
import { Button } from '../../button';
|
|
21
22
|
import {
|
|
22
23
|
Dialog,
|
|
@@ -226,9 +227,12 @@ export function SchedulingDialog({
|
|
|
226
227
|
queryKey: ['task-personal-schedule', task?.id, open],
|
|
227
228
|
enabled: open && !!task?.id,
|
|
228
229
|
queryFn: async () => {
|
|
229
|
-
const res = await fetch(
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
const res = await fetch(
|
|
231
|
+
getTaskApiUrl(`/api/v1/users/me/tasks/${task!.id}/schedule`),
|
|
232
|
+
{
|
|
233
|
+
cache: 'no-store',
|
|
234
|
+
}
|
|
235
|
+
);
|
|
232
236
|
if (!res.ok) return null;
|
|
233
237
|
return (await res.json()) as null | {
|
|
234
238
|
task: {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import type { TaskWithScheduling } from '@tuturuuu/types';
|
|
16
16
|
import { cn } from '@tuturuuu/utils/format';
|
|
17
17
|
import { useState } from 'react';
|
|
18
|
+
import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
|
|
18
19
|
import { Badge } from '../../badge';
|
|
19
20
|
import { Button } from '../../button';
|
|
20
21
|
import { Input } from '../../input';
|
|
@@ -147,11 +148,14 @@ export function TaskSchedulerPanel({
|
|
|
147
148
|
|
|
148
149
|
try {
|
|
149
150
|
const response = await fetch(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
getTaskApiUrl(
|
|
152
|
+
isPersonalWorkspace
|
|
153
|
+
? `/api/v1/users/me/tasks/${task.id}/schedule`
|
|
154
|
+
: `/api/v1/workspaces/${wsId}/tasks/${task.id}/schedule`
|
|
155
|
+
),
|
|
153
156
|
{
|
|
154
157
|
method: 'POST',
|
|
158
|
+
credentials: 'include',
|
|
155
159
|
headers: { 'Content-Type': 'application/json' },
|
|
156
160
|
}
|
|
157
161
|
);
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import type { TimeTrackingCategory } from '@tuturuuu/types';
|
|
18
18
|
import { cn } from '@tuturuuu/utils/format';
|
|
19
19
|
import { useCallback, useEffect, useState } from 'react';
|
|
20
|
+
import { getTaskApiUrl } from '../../../../../lib/tasks-app-url';
|
|
20
21
|
import { Badge } from '../../../badge';
|
|
21
22
|
import { Button } from '../../../button';
|
|
22
23
|
import { Card, CardContent, CardHeader, CardTitle } from '../../../card';
|
|
@@ -201,15 +202,19 @@ export default function TimeTracker({ wsId, tasks = [] }: TimeTrackerProps) {
|
|
|
201
202
|
setIsCreatingTask(true);
|
|
202
203
|
|
|
203
204
|
try {
|
|
204
|
-
const response = await fetch(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
205
|
+
const response = await fetch(
|
|
206
|
+
getTaskApiUrl(`/api/v1/workspaces/${wsId}/tasks`),
|
|
207
|
+
{
|
|
208
|
+
method: 'POST',
|
|
209
|
+
credentials: 'include',
|
|
210
|
+
headers: { 'Content-Type': 'application/json' },
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
name: newTaskName,
|
|
213
|
+
description: newTaskDescription || null,
|
|
214
|
+
listId: selectedListId,
|
|
215
|
+
}),
|
|
216
|
+
}
|
|
217
|
+
);
|
|
213
218
|
|
|
214
219
|
if (!response.ok) throw new Error('Failed to create task');
|
|
215
220
|
|
|
@@ -27,6 +27,7 @@ import { toast } from '@tuturuuu/ui/sonner';
|
|
|
27
27
|
import { Switch } from '@tuturuuu/ui/switch';
|
|
28
28
|
import { useTranslations } from 'next-intl';
|
|
29
29
|
import { useEffect } from 'react';
|
|
30
|
+
import { getTaskApiUrl } from '../../../../lib/tasks-app-url';
|
|
30
31
|
import {
|
|
31
32
|
normalizeTaskDialogPresentation,
|
|
32
33
|
TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID,
|
|
@@ -145,7 +146,9 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
|
|
|
145
146
|
const { data: settings, isLoading } = useQuery({
|
|
146
147
|
queryKey: ['user-task-settings'],
|
|
147
148
|
queryFn: async (): Promise<TaskSettingsData> => {
|
|
148
|
-
const res = await fetch('/api/v1/users/task-settings')
|
|
149
|
+
const res = await fetch(getTaskApiUrl('/api/v1/users/task-settings'), {
|
|
150
|
+
credentials: 'include',
|
|
151
|
+
});
|
|
149
152
|
if (!res.ok) throw new Error('Failed to fetch task settings');
|
|
150
153
|
return res.json();
|
|
151
154
|
},
|
|
@@ -162,8 +165,9 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
|
|
|
162
165
|
|
|
163
166
|
const updateSettings = useMutation({
|
|
164
167
|
mutationFn: async (data: Partial<TaskSettingsData>) => {
|
|
165
|
-
const res = await fetch('/api/v1/users/task-settings', {
|
|
168
|
+
const res = await fetch(getTaskApiUrl('/api/v1/users/task-settings'), {
|
|
166
169
|
method: 'PATCH',
|
|
170
|
+
credentials: 'include',
|
|
167
171
|
headers: { 'Content-Type': 'application/json' },
|
|
168
172
|
body: JSON.stringify(data),
|
|
169
173
|
});
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getFacetedUniqueValues,
|
|
10
10
|
getFilteredRowModel,
|
|
11
11
|
getSortedRowModel,
|
|
12
|
+
type Row,
|
|
12
13
|
type SortingState,
|
|
13
14
|
useReactTable,
|
|
14
15
|
type VisibilityState,
|
|
@@ -27,6 +28,15 @@ import {
|
|
|
27
28
|
import { DataTablePagination } from './data-table-pagination';
|
|
28
29
|
import { DataTableToolbar } from './data-table-toolbar';
|
|
29
30
|
|
|
31
|
+
function isInteractiveTarget(target: EventTarget | null) {
|
|
32
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
33
|
+
return Boolean(
|
|
34
|
+
target.closest(
|
|
35
|
+
'a, button, input, select, textarea, label, [role="menuitem"], [data-no-row-activate]'
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
/**
|
|
31
41
|
* Options for column generator functions.
|
|
32
42
|
* Using named parameters improves readability and maintainability.
|
|
@@ -79,6 +89,8 @@ export interface DataTableProps<TData, TValue> {
|
|
|
79
89
|
/** Custom toolbar actions rendered directly (not wrapped in dialogs) */
|
|
80
90
|
toolbarActions?: ReactNode;
|
|
81
91
|
className?: string;
|
|
92
|
+
tableCardClassName?: string;
|
|
93
|
+
tableClassName?: string;
|
|
82
94
|
preserveParams?: string[];
|
|
83
95
|
onRefresh?: () => void;
|
|
84
96
|
selectedRowsActions?: (selectedRows: TData[]) => ReactNode;
|
|
@@ -87,6 +99,7 @@ export interface DataTableProps<TData, TValue> {
|
|
|
87
99
|
|
|
88
100
|
onRowClick?: (row: TData) => void;
|
|
89
101
|
onRowDoubleClick?: (row: TData) => void;
|
|
102
|
+
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
|
|
90
103
|
|
|
91
104
|
setParams?: (params: {
|
|
92
105
|
page?: number;
|
|
@@ -127,11 +140,14 @@ export function DataTable<TData, TValue>({
|
|
|
127
140
|
toolbarExportContent,
|
|
128
141
|
toolbarActions,
|
|
129
142
|
className,
|
|
143
|
+
tableCardClassName,
|
|
144
|
+
tableClassName,
|
|
130
145
|
onRefresh,
|
|
131
146
|
selectedRowsActions,
|
|
132
147
|
onSearch,
|
|
133
148
|
onRowClick,
|
|
134
149
|
onRowDoubleClick,
|
|
150
|
+
getRowId,
|
|
135
151
|
setParams,
|
|
136
152
|
resetParams,
|
|
137
153
|
columnGenerator,
|
|
@@ -169,6 +185,7 @@ export function DataTable<TData, TValue>({
|
|
|
169
185
|
: undefined,
|
|
170
186
|
enableRowSelection: true,
|
|
171
187
|
autoResetPageIndex: true,
|
|
188
|
+
getRowId,
|
|
172
189
|
onRowSelectionChange: setRowSelection,
|
|
173
190
|
onSortingChange: enableServerSideSorting
|
|
174
191
|
? (updaterOrValue) => {
|
|
@@ -232,16 +249,23 @@ export function DataTable<TData, TValue>({
|
|
|
232
249
|
toolbarActions={toolbarActions}
|
|
233
250
|
/>
|
|
234
251
|
)}
|
|
235
|
-
<Card>
|
|
236
|
-
<Table>
|
|
252
|
+
<Card className={tableCardClassName}>
|
|
253
|
+
<Table className={tableClassName}>
|
|
237
254
|
<TableHeader>
|
|
238
255
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
239
256
|
<TableRow key={headerGroup.id}>
|
|
240
257
|
{headerGroup.headers.map((header) => {
|
|
258
|
+
const meta = header.column.columnDef.meta as
|
|
259
|
+
| { className?: string }
|
|
260
|
+
| undefined;
|
|
261
|
+
|
|
241
262
|
return (
|
|
242
263
|
<TableHead
|
|
243
264
|
key={header.id}
|
|
244
|
-
className=
|
|
265
|
+
className={cn(
|
|
266
|
+
'bg-foreground/5 font-semibold text-foreground/70 first:rounded-tl-xl last:rounded-tr-xl',
|
|
267
|
+
meta?.className
|
|
268
|
+
)}
|
|
245
269
|
>
|
|
246
270
|
{header.isPlaceholder
|
|
247
271
|
? null
|
|
@@ -258,16 +282,42 @@ export function DataTable<TData, TValue>({
|
|
|
258
282
|
<TableBody>
|
|
259
283
|
{table.getRowModel().rows?.length ? (
|
|
260
284
|
table.getRowModel().rows.map((row) => {
|
|
285
|
+
const isInteractiveRow = Boolean(
|
|
286
|
+
onRowClick || onRowDoubleClick
|
|
287
|
+
);
|
|
261
288
|
const tableRow = (
|
|
262
289
|
<TableRow
|
|
263
290
|
key={`${namespace}-${row.id}`}
|
|
264
291
|
data-state={row.getIsSelected() && 'selected'}
|
|
265
|
-
className=
|
|
266
|
-
onClick={() =>
|
|
267
|
-
|
|
292
|
+
className={cn(isInteractiveRow && 'cursor-pointer')}
|
|
293
|
+
onClick={(event) => {
|
|
294
|
+
if (isInteractiveTarget(event.target)) return;
|
|
295
|
+
onRowClick?.(row.original);
|
|
296
|
+
}}
|
|
297
|
+
onDoubleClick={(event) => {
|
|
298
|
+
if (isInteractiveTarget(event.target)) return;
|
|
299
|
+
onRowDoubleClick?.(row.original);
|
|
300
|
+
}}
|
|
301
|
+
onKeyDown={(event) => {
|
|
302
|
+
if (!onRowClick) return;
|
|
303
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
304
|
+
if (isInteractiveTarget(event.target)) return;
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
onRowClick(row.original);
|
|
307
|
+
}}
|
|
308
|
+
tabIndex={onRowClick ? 0 : undefined}
|
|
268
309
|
>
|
|
269
310
|
{row.getVisibleCells().map((cell) => (
|
|
270
|
-
<TableCell
|
|
311
|
+
<TableCell
|
|
312
|
+
key={`${namespace}-${cell.id}`}
|
|
313
|
+
className={
|
|
314
|
+
(
|
|
315
|
+
cell.column.columnDef.meta as
|
|
316
|
+
| { cellClassName?: string }
|
|
317
|
+
| undefined
|
|
318
|
+
)?.cellClassName
|
|
319
|
+
}
|
|
320
|
+
>
|
|
271
321
|
{flexRender(
|
|
272
322
|
cell.column.columnDef.cell,
|
|
273
323
|
cell.getContext()
|
|
@@ -293,7 +343,7 @@ export function DataTable<TData, TValue>({
|
|
|
293
343
|
>
|
|
294
344
|
{data
|
|
295
345
|
? `${t?.('common.no-results')}.`
|
|
296
|
-
: `${t?.('common.loading')}
|
|
346
|
+
: `${t?.('common.loading')}…`}
|
|
297
347
|
</TableCell>
|
|
298
348
|
</TableRow>
|
|
299
349
|
)}
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { QueryClient } from '@tanstack/react-query';
|
|
2
2
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
3
3
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
vi.mock('@tuturuuu/utils/task-helper', () => {
|
|
7
|
+
const prefix = 'personal-external-staging:';
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
getPersonalExternalStagingBoardId: (listId: string | null) =>
|
|
11
|
+
listId?.startsWith(prefix) && listId.length > prefix.length
|
|
12
|
+
? listId.slice(prefix.length)
|
|
13
|
+
: null,
|
|
14
|
+
getPersonalExternalStagingListId: (boardId: string) =>
|
|
15
|
+
`${prefix}${boardId}`,
|
|
16
|
+
isPersonalExternalStagingListId: (listId: string | null) =>
|
|
17
|
+
Boolean(listId?.startsWith(prefix) && listId.length > prefix.length),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
5
21
|
import {
|
|
6
22
|
applyTaskDropPreviewToCache,
|
|
7
23
|
getProjectedTaskDropOrderFromPreview,
|
|
@@ -14,6 +30,7 @@ import {
|
|
|
14
30
|
insertTaskAtDropPosition,
|
|
15
31
|
mergePersonalPlacementMutationTask,
|
|
16
32
|
mergeTaskIntoBoardTaskCache,
|
|
33
|
+
usesPersonalPlacement,
|
|
17
34
|
} from './use-kanban-dnd';
|
|
18
35
|
|
|
19
36
|
function createTask(overrides: Partial<Task> = {}): Task {
|
|
@@ -116,6 +133,51 @@ describe('mergePersonalPlacementMutationTask', () => {
|
|
|
116
133
|
});
|
|
117
134
|
});
|
|
118
135
|
|
|
136
|
+
describe('usesPersonalPlacement', () => {
|
|
137
|
+
it('does not treat a personal-workspace task as external from personal_board_id alone', () => {
|
|
138
|
+
const personalTask = createTask({
|
|
139
|
+
is_personal_external: false,
|
|
140
|
+
personal_board_id: 'personal-board',
|
|
141
|
+
personal_list_id: 'list-1',
|
|
142
|
+
source_board_id: null,
|
|
143
|
+
source_list_id: null,
|
|
144
|
+
source_workspace_id: null,
|
|
145
|
+
} as Partial<Task>);
|
|
146
|
+
|
|
147
|
+
expect(usesPersonalPlacement(personalTask)).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('does not treat an explicitly native personal task as external when source metadata is present', () => {
|
|
151
|
+
const personalTask = createTask({
|
|
152
|
+
is_personal_external: false,
|
|
153
|
+
personal_board_id: 'personal-board',
|
|
154
|
+
personal_list_id: 'list-1',
|
|
155
|
+
source_workspace_id: 'personal-workspace',
|
|
156
|
+
source_board_id: 'personal-board',
|
|
157
|
+
source_list_id: 'list-1',
|
|
158
|
+
} as Partial<Task>);
|
|
159
|
+
|
|
160
|
+
expect(usesPersonalPlacement(personalTask)).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('does not treat same-board personal metadata as external without an explicit flag', () => {
|
|
164
|
+
const personalTask = createTask({
|
|
165
|
+
is_personal_external: undefined,
|
|
166
|
+
personal_board_id: 'personal-board',
|
|
167
|
+
personal_list_id: 'list-1',
|
|
168
|
+
source_workspace_id: 'personal-workspace',
|
|
169
|
+
source_board_id: 'personal-board',
|
|
170
|
+
source_list_id: 'list-1',
|
|
171
|
+
} as Partial<Task>);
|
|
172
|
+
|
|
173
|
+
expect(usesPersonalPlacement(personalTask)).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('keeps explicit external overlays on the personal-placement path', () => {
|
|
177
|
+
expect(usesPersonalPlacement(createTask())).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
119
181
|
describe('mergeTaskIntoBoardTaskCache', () => {
|
|
120
182
|
it('updates an existing task in the board cache', () => {
|
|
121
183
|
const existingTask = createTask({ list_id: 'source-list' });
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from '@tuturuuu/utils/task-helper';
|
|
22
22
|
import { hasDraggableData } from '@tuturuuu/utils/task-helpers';
|
|
23
23
|
import { useCallback, useRef, useState } from 'react';
|
|
24
|
+
import { isPersonalExternalOverlayTask } from '../../../../../../../lib/task-personal-external';
|
|
24
25
|
import { useBoardBroadcast } from '../../../../shared/board-broadcast-context';
|
|
25
26
|
import { invalidateKanbanDeadlineTasks } from '../data/kanban-deadline-query';
|
|
26
27
|
import { MAX_SAFE_INTEGER_SORT } from '../kanban-constants';
|
|
@@ -121,12 +122,8 @@ interface UseKanbanDndProps {
|
|
|
121
122
|
scrollContainerRef: React.RefObject<HTMLDivElement | null>;
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
function usesPersonalPlacement(task: Task) {
|
|
125
|
-
return (
|
|
126
|
-
task.is_personal_external === true ||
|
|
127
|
-
Boolean(task.personal_board_id) ||
|
|
128
|
-
isPersonalExternalStagingListId(task.list_id)
|
|
129
|
-
);
|
|
125
|
+
export function usesPersonalPlacement(task: Task) {
|
|
126
|
+
return isPersonalExternalOverlayTask(task);
|
|
130
127
|
}
|
|
131
128
|
|
|
132
129
|
function getTaskDropPosition(
|
|
@@ -76,7 +76,7 @@ describe('getTaskCardHydratingOpenOptions', () => {
|
|
|
76
76
|
taskWsId: 'source-workspace',
|
|
77
77
|
taskWorkspacePersonal: false,
|
|
78
78
|
canUseBoardAssignees: true,
|
|
79
|
-
assigneeMemberSource: '
|
|
79
|
+
assigneeMemberSource: 'board',
|
|
80
80
|
initialSharedContext: {
|
|
81
81
|
boardConfig: {
|
|
82
82
|
id: 'source-board',
|
|
@@ -151,4 +151,14 @@ describe('getTaskCardHydratingOpenOptions', () => {
|
|
|
151
151
|
).toBe(true);
|
|
152
152
|
expect(isExternalTaskSnapshot(task)).toBe(false);
|
|
153
153
|
});
|
|
154
|
+
|
|
155
|
+
it('does not treat personal placement metadata alone as an external snapshot', () => {
|
|
156
|
+
expect(
|
|
157
|
+
isExternalTaskSnapshot({
|
|
158
|
+
...task,
|
|
159
|
+
personal_board_id: 'board-1',
|
|
160
|
+
personal_list_id: 'list-1',
|
|
161
|
+
})
|
|
162
|
+
).toBe(false);
|
|
163
|
+
});
|
|
154
164
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
2
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
3
|
+
import { isPersonalExternalOverlayTask } from '../../../../../../lib/task-personal-external';
|
|
3
4
|
import type { SharedTaskContext } from '../../../shared/task-edit-dialog/hooks/use-task-data';
|
|
4
5
|
|
|
5
6
|
interface TaskCardOpenOptionsInput {
|
|
@@ -13,9 +14,12 @@ interface TaskCardOpenOptionsInput {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function isExternalTaskSnapshot(task: Task) {
|
|
17
|
+
if (task.is_personal_external === false) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
return (
|
|
17
|
-
task
|
|
18
|
-
Boolean(task.personal_board_id) ||
|
|
22
|
+
isPersonalExternalOverlayTask(task) ||
|
|
19
23
|
Boolean(task.source_workspace_id) ||
|
|
20
24
|
Boolean(task.source_board_id)
|
|
21
25
|
);
|
|
@@ -131,7 +135,7 @@ export function getTaskCardHydratingOpenOptions({
|
|
|
131
135
|
taskWorkspacePersonal: sourceWorkspaceId ? false : isPersonalWorkspace,
|
|
132
136
|
canUseBoardAssignees: canUseBoardAssignees ?? sourceBoardAssigneesEnabled,
|
|
133
137
|
assigneeMemberSource:
|
|
134
|
-
assigneeMemberSource ?? (sourceWorkspaceId ? '
|
|
138
|
+
assigneeMemberSource ?? (sourceWorkspaceId ? 'board' : undefined),
|
|
135
139
|
initialSharedContext,
|
|
136
140
|
};
|
|
137
141
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
|
+
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { getTaskCardResourceContext } from './task-card-resource-context';
|
|
5
|
+
|
|
6
|
+
const pageList = {
|
|
7
|
+
id: 'page-list',
|
|
8
|
+
name: 'Page list',
|
|
9
|
+
board_id: 'page-board',
|
|
10
|
+
position: 0,
|
|
11
|
+
status: 'not_started',
|
|
12
|
+
color: 'BLUE',
|
|
13
|
+
created_at: '2026-07-04T00:00:00.000Z',
|
|
14
|
+
creator_id: 'user-1',
|
|
15
|
+
archived: false,
|
|
16
|
+
deleted: false,
|
|
17
|
+
} satisfies TaskList;
|
|
18
|
+
|
|
19
|
+
const task = {
|
|
20
|
+
id: 'task-1',
|
|
21
|
+
name: 'Task',
|
|
22
|
+
description: '',
|
|
23
|
+
list_id: 'page-list',
|
|
24
|
+
display_number: 1,
|
|
25
|
+
created_at: '2026-07-04T00:00:00.000Z',
|
|
26
|
+
end_date: null,
|
|
27
|
+
priority: 'normal',
|
|
28
|
+
} satisfies Task;
|
|
29
|
+
|
|
30
|
+
describe('getTaskCardResourceContext', () => {
|
|
31
|
+
it('keeps local task resources on the page workspace and board', () => {
|
|
32
|
+
expect(
|
|
33
|
+
getTaskCardResourceContext({
|
|
34
|
+
boardId: 'page-board',
|
|
35
|
+
pageWorkspaceId: 'page-workspace',
|
|
36
|
+
propAvailableLists: [pageList],
|
|
37
|
+
task,
|
|
38
|
+
})
|
|
39
|
+
).toEqual({
|
|
40
|
+
boardViewableMembersBoardId: 'page-board',
|
|
41
|
+
boardViewableMembersWorkspaceId: 'page-workspace',
|
|
42
|
+
effectiveWorkspaceId: 'page-workspace',
|
|
43
|
+
initialAvailableLists: [pageList],
|
|
44
|
+
isSourceWorkspaceTask: false,
|
|
45
|
+
taskBoardId: 'page-board',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('routes source task labels, projects, lists, and members to the source workspace and board', () => {
|
|
50
|
+
expect(
|
|
51
|
+
getTaskCardResourceContext({
|
|
52
|
+
boardId: 'personal-board',
|
|
53
|
+
pageWorkspaceId: 'personal-workspace',
|
|
54
|
+
propAvailableLists: [pageList],
|
|
55
|
+
task: {
|
|
56
|
+
...task,
|
|
57
|
+
list_id: 'personal-list',
|
|
58
|
+
source_board_id: 'source-board',
|
|
59
|
+
source_workspace_id: 'source-workspace',
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
).toEqual({
|
|
63
|
+
boardViewableMembersBoardId: 'source-board',
|
|
64
|
+
boardViewableMembersWorkspaceId: 'source-workspace',
|
|
65
|
+
effectiveWorkspaceId: 'source-workspace',
|
|
66
|
+
initialAvailableLists: undefined,
|
|
67
|
+
isSourceWorkspaceTask: true,
|
|
68
|
+
taskBoardId: 'source-board',
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('uses the page workspace when a source board is present without source workspace metadata', () => {
|
|
73
|
+
expect(
|
|
74
|
+
getTaskCardResourceContext({
|
|
75
|
+
boardId: 'page-board',
|
|
76
|
+
pageWorkspaceId: 'page-workspace',
|
|
77
|
+
propAvailableLists: [pageList],
|
|
78
|
+
task: {
|
|
79
|
+
...task,
|
|
80
|
+
source_board_id: 'source-board',
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
).toMatchObject({
|
|
84
|
+
boardViewableMembersBoardId: 'source-board',
|
|
85
|
+
boardViewableMembersWorkspaceId: 'page-workspace',
|
|
86
|
+
effectiveWorkspaceId: 'page-workspace',
|
|
87
|
+
initialAvailableLists: undefined,
|
|
88
|
+
isSourceWorkspaceTask: true,
|
|
89
|
+
taskBoardId: 'source-board',
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('keeps page board list data when only source workspace metadata is available', () => {
|
|
94
|
+
expect(
|
|
95
|
+
getTaskCardResourceContext({
|
|
96
|
+
boardId: 'page-board',
|
|
97
|
+
pageWorkspaceId: 'page-workspace',
|
|
98
|
+
propAvailableLists: [pageList],
|
|
99
|
+
task: {
|
|
100
|
+
...task,
|
|
101
|
+
source_workspace_id: 'source-workspace',
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
).toMatchObject({
|
|
105
|
+
boardViewableMembersBoardId: 'page-board',
|
|
106
|
+
boardViewableMembersWorkspaceId: 'source-workspace',
|
|
107
|
+
effectiveWorkspaceId: 'source-workspace',
|
|
108
|
+
initialAvailableLists: [pageList],
|
|
109
|
+
isSourceWorkspaceTask: true,
|
|
110
|
+
taskBoardId: 'page-board',
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|