@tuturuuu/ui 0.14.0 → 0.17.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 +96 -0
- package/biome.json +1 -1
- package/package.json +63 -62
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +12 -0
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +93 -6
- package/src/components/ui/custom/loading-statistic-card.tsx +18 -0
- package/src/components/ui/custom/notification-popover-client.tsx +5 -1
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.test.tsx +34 -0
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +106 -0
- package/src/components/ui/custom/settings/sidebar-settings.tsx +18 -6
- package/src/components/ui/custom/settings-dialog-shell.tsx +18 -12
- package/src/components/ui/custom/sidebar-context.tsx +58 -3
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +5 -3
- package/src/components/ui/custom/statistic-card.tsx +104 -0
- package/src/components/ui/custom/structure.test.tsx +29 -0
- package/src/components/ui/custom/structure.tsx +103 -88
- package/src/components/ui/finance/invoices/civil-month.ts +224 -0
- package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.test.tsx +49 -0
- package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.tsx +81 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +90 -2
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +40 -4
- package/src/components/ui/finance/invoices/pending-columns.test.ts +26 -0
- package/src/components/ui/finance/invoices/pending-columns.tsx +45 -14
- package/src/components/ui/finance/invoices/standard-invoice.test.ts +31 -0
- package/src/components/ui/finance/invoices/standard-invoice.tsx +33 -16
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +331 -0
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +53 -28
- package/src/components/ui/finance/invoices/utils.test.ts +84 -0
- package/src/components/ui/finance/invoices/utils.ts +56 -134
- package/src/components/ui/resizable.test.ts +15 -0
- package/src/components/ui/resizable.tsx +28 -6
- package/src/components/ui/storefront/browse-panel.tsx +161 -0
- package/src/components/ui/storefront/bundle-selection-dialog.tsx +1 -1
- package/src/components/ui/storefront/cart-popover.tsx +1 -1
- package/src/components/ui/storefront/hero-panel.tsx +35 -30
- package/src/components/ui/storefront/listing-card.tsx +10 -5
- package/src/components/ui/storefront/merch-sections.tsx +7 -5
- package/src/components/ui/storefront/product-detail.tsx +79 -6
- package/src/components/ui/storefront/product-dialog.tsx +5 -1
- package/src/components/ui/storefront/storefront-surface.test.tsx +72 -0
- package/src/components/ui/storefront/storefront-surface.tsx +67 -63
- package/src/components/ui/storefront/types.ts +28 -0
- package/src/components/ui/storefront/utils.ts +4 -5
- package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +59 -1
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +37 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +62 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +48 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +32 -2
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +41 -3
- package/src/components/ui/tu-do/boards/boardId/menus/task-due-date-menu.tsx +31 -10
- package/src/components/ui/tu-do/boards/boardId/menus/task-priority-menu.tsx +7 -5
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +7 -5
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.test.ts +44 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +44 -30
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +54 -0
- package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +43 -1
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +33 -15
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +3 -3
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +60 -26
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +12 -10
- package/src/components/ui/tu-do/projects/components/project-grid-card.tsx +22 -6
- package/src/components/ui/tu-do/projects/components/project-list-item.tsx +24 -8
- package/src/components/ui/tu-do/projects/projectId/components/__tests__/tasks-tab-layout.test.tsx +6 -0
- package/src/components/ui/tu-do/projects/projectId/components/tasks-tab.tsx +3 -1
- package/src/components/ui/tu-do/projects/projectId/task-project-detail-page-client.tsx +11 -3
- package/src/components/ui/tu-do/projects/projectId/task-project-detail.tsx +2 -0
- package/src/components/ui/tu-do/projects/projectId/types.ts +1 -0
- package/src/components/ui/tu-do/projects/task-projects-client.tsx +45 -1
- package/src/components/ui/tu-do/projects/types.ts +4 -1
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +10 -0
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +87 -2
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +27 -3
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +41 -3
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +54 -0
- package/src/components/ui/tu-do/shared/board-header.tsx +20 -0
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +2 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +7 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-mutations.test.tsx +231 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +139 -68
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.test.tsx +68 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +17 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +45 -23
- package/src/components/ui/tu-do/shared/task-row-actions-menu.test.tsx +138 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +15 -9
- package/src/components/ui/tu-do/templates/client.tsx +97 -78
- package/src/components/ui/tu-do/templates/marketplace/client.tsx +171 -135
- package/src/components/ui/tu-do/templates/task-templates-hub.tsx +3 -0
- package/src/components/ui/tu-do/templates/templateId/client.tsx +24 -9
- package/src/components/ui/tu-do/templates/templateId/task-template-detail-page-client.tsx +8 -2
- package/src/globals.css +1 -0
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +14 -39
- package/src/hooks/task-actions-personal-external.ts +12 -6
- package/src/hooks/use-notifications.ts +14 -2
- package/vitest.config.ts +4 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
listWorkspaceTaskLists,
|
|
8
8
|
removeWorkspaceTaskLabel,
|
|
9
9
|
updateWorkspaceTask,
|
|
10
|
+
upsertCurrentUserTaskPersonalPlacement,
|
|
10
11
|
} from '@tuturuuu/internal-api';
|
|
11
12
|
import type { TaskWithRelations } from '@tuturuuu/types';
|
|
12
13
|
import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
|
|
@@ -221,23 +222,40 @@ export function useTaskContextActions({
|
|
|
221
222
|
if (!taskBoardId || !taskWorkspaceId) return;
|
|
222
223
|
setIsLoading(true);
|
|
223
224
|
try {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
const personalPlacement = task.overrides as
|
|
226
|
+
| (NonNullable<typeof task.overrides> & {
|
|
227
|
+
personal_board_id?: string | null;
|
|
228
|
+
personal_list_id?: string | null;
|
|
229
|
+
})
|
|
230
|
+
| null;
|
|
231
|
+
const personalBoardId = personalPlacement?.personal_board_id ?? null;
|
|
228
232
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
233
|
+
if (personalBoardId) {
|
|
234
|
+
await upsertCurrentUserTaskPersonalPlacement(task.id, {
|
|
235
|
+
personal_board_id: personalBoardId,
|
|
236
|
+
personal_list_id: personalPlacement?.personal_list_id ?? null,
|
|
237
|
+
terminal_status: 'done',
|
|
238
|
+
});
|
|
239
|
+
broadcastTaskUpsert({});
|
|
240
|
+
} else {
|
|
241
|
+
const { lists } = await listWorkspaceTaskLists(
|
|
242
|
+
taskWorkspaceId,
|
|
243
|
+
taskBoardId
|
|
244
|
+
);
|
|
234
245
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
246
|
+
const doneList = lists?.find((l) => l.status === 'done');
|
|
247
|
+
if (!doneList) {
|
|
248
|
+
toast.error('No done list found');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await updateWorkspaceTask(taskWorkspaceId, task.id, {
|
|
253
|
+
list_id: doneList.id,
|
|
254
|
+
});
|
|
255
|
+
broadcastTaskUpsert({
|
|
256
|
+
list_id: doneList.id,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
241
259
|
|
|
242
260
|
// Clear redundant personal overrides when task is actually done
|
|
243
261
|
if (
|
|
@@ -19,7 +19,7 @@ function MetricSelect({
|
|
|
19
19
|
}) {
|
|
20
20
|
return (
|
|
21
21
|
<select
|
|
22
|
-
className="h-
|
|
22
|
+
className="h-11 w-full rounded-xl border bg-background px-3 text-sm shadow-sm outline-none focus:border-dynamic-blue focus:ring-2 focus:ring-dynamic-blue/15"
|
|
23
23
|
defaultValue={selectedMetric?.id}
|
|
24
24
|
name="metric_id"
|
|
25
25
|
required
|
|
@@ -55,8 +55,8 @@ export function LeaderboardsPanel(props: {
|
|
|
55
55
|
} = props;
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
|
-
<div className="grid gap-4
|
|
59
|
-
<Card>
|
|
58
|
+
<div className="grid gap-4 xl:grid-cols-[minmax(20rem,0.75fr)_minmax(0,1.25fr)]">
|
|
59
|
+
<Card className="h-fit xl:sticky xl:top-6">
|
|
60
60
|
<CardHeader>
|
|
61
61
|
<CardTitle>{t('leaderboards.create_leaderboard')}</CardTitle>
|
|
62
62
|
</CardHeader>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
|
-
import { BarChart3, Flag, Target,
|
|
4
|
+
import { BarChart3, Flag, Target, Trophy, Upload } from '@tuturuuu/icons';
|
|
5
5
|
import {
|
|
6
6
|
createTaskLeaderboard,
|
|
7
7
|
createTaskLeaderboardTeam,
|
|
@@ -95,6 +95,7 @@ export function TaskProgressPage({
|
|
|
95
95
|
const queryClient = useQueryClient();
|
|
96
96
|
const [importText, setImportText] = useState('');
|
|
97
97
|
const [importPreviewCount, setImportPreviewCount] = useState(0);
|
|
98
|
+
const [selectedMetricId, setSelectedMetricId] = useState<string | null>(null);
|
|
98
99
|
const queryRoot = ['task-progress', wsId];
|
|
99
100
|
|
|
100
101
|
const metricsQuery = useQuery({
|
|
@@ -103,7 +104,12 @@ export function TaskProgressPage({
|
|
|
103
104
|
});
|
|
104
105
|
const metrics =
|
|
105
106
|
metricsQuery.data?.ok === true ? metricsQuery.data.metrics : [];
|
|
106
|
-
const selectedMetric = useMemo(
|
|
107
|
+
const selectedMetric = useMemo(
|
|
108
|
+
() =>
|
|
109
|
+
metrics.find((metric) => metric.id === selectedMetricId) ??
|
|
110
|
+
metricOption(metrics),
|
|
111
|
+
[metrics, selectedMetricId]
|
|
112
|
+
);
|
|
107
113
|
|
|
108
114
|
const entriesQuery = useQuery({
|
|
109
115
|
queryKey: [...queryRoot, 'entries', selectedMetric?.id],
|
|
@@ -236,42 +242,70 @@ export function TaskProgressPage({
|
|
|
236
242
|
const leaderboards = leaderboardsQuery.data?.ok
|
|
237
243
|
? leaderboardsQuery.data.leaderboards
|
|
238
244
|
: [];
|
|
245
|
+
const tabs = [
|
|
246
|
+
{ icon: Flag, value: 'progress' },
|
|
247
|
+
{ icon: Target, value: 'goals' },
|
|
248
|
+
{ icon: BarChart3, value: 'stats' },
|
|
249
|
+
{ icon: Trophy, value: 'leaderboards' },
|
|
250
|
+
{ icon: Upload, value: 'import' },
|
|
251
|
+
] as const;
|
|
239
252
|
|
|
240
253
|
return (
|
|
241
|
-
<div className="flex flex-col gap-6 p-4 md:p-6">
|
|
242
|
-
<
|
|
243
|
-
<div className="
|
|
244
|
-
<div className="
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
</
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
<div className="mx-auto flex w-full max-w-[1600px] flex-col gap-6 p-4 md:p-6 lg:p-8">
|
|
255
|
+
<section className="overflow-hidden rounded-2xl border bg-card/60 shadow-sm backdrop-blur">
|
|
256
|
+
<div className="flex flex-col gap-4 p-5 md:flex-row md:items-center md:justify-between md:p-6">
|
|
257
|
+
<div className="max-w-2xl">
|
|
258
|
+
<h1 className="font-bold text-2xl tracking-tight md:text-3xl">
|
|
259
|
+
{t('views.progress.title')}
|
|
260
|
+
</h1>
|
|
261
|
+
<p className="mt-1 text-muted-foreground text-sm md:text-base">
|
|
262
|
+
{t(`views.${view}.description`)}
|
|
263
|
+
</p>
|
|
264
|
+
</div>
|
|
265
|
+
<div className="shrink-0">
|
|
266
|
+
{metrics.length > 0 ? (
|
|
267
|
+
<label className="flex min-w-56 items-center justify-between gap-3 rounded-xl border bg-background px-3 py-2 text-sm shadow-sm">
|
|
268
|
+
<span className="text-muted-foreground">
|
|
269
|
+
{t('fields.metric_name')}
|
|
270
|
+
</span>
|
|
271
|
+
<select
|
|
272
|
+
className="min-w-28 bg-transparent text-right font-medium outline-none"
|
|
273
|
+
onChange={(event) => setSelectedMetricId(event.target.value)}
|
|
274
|
+
value={selectedMetric?.id ?? ''}
|
|
275
|
+
>
|
|
276
|
+
{metrics.map((metric) => (
|
|
277
|
+
<option key={metric.id} value={metric.id}>
|
|
278
|
+
{metric.name}
|
|
279
|
+
</option>
|
|
280
|
+
))}
|
|
281
|
+
</select>
|
|
282
|
+
</label>
|
|
283
|
+
) : null}
|
|
256
284
|
</div>
|
|
257
285
|
</div>
|
|
258
|
-
<
|
|
259
|
-
{(
|
|
260
|
-
['progress', 'goals', 'stats', 'leaderboards', 'import'] as const
|
|
261
|
-
).map((tab) => (
|
|
286
|
+
<nav className="flex max-w-full gap-1 overflow-x-auto border-t bg-muted/20 p-2">
|
|
287
|
+
{tabs.map(({ icon: TabIcon, value: tab }) => (
|
|
262
288
|
<Button
|
|
263
289
|
key={tab}
|
|
264
290
|
asChild
|
|
291
|
+
className="shrink-0 gap-2 rounded-lg"
|
|
265
292
|
size="sm"
|
|
266
|
-
variant={tab === view ? '
|
|
293
|
+
variant={tab === view ? 'secondary' : 'ghost'}
|
|
267
294
|
>
|
|
268
|
-
<Link
|
|
295
|
+
<Link
|
|
296
|
+
href={
|
|
297
|
+
tab === 'progress'
|
|
298
|
+
? `/${routeWsId}/progress`
|
|
299
|
+
: `/${routeWsId}/progress/${tab}`
|
|
300
|
+
}
|
|
301
|
+
>
|
|
302
|
+
<TabIcon className="size-4" />
|
|
269
303
|
{t(`tabs.${tab}`)}
|
|
270
304
|
</Link>
|
|
271
305
|
</Button>
|
|
272
306
|
))}
|
|
273
|
-
</
|
|
274
|
-
</
|
|
307
|
+
</nav>
|
|
308
|
+
</section>
|
|
275
309
|
|
|
276
310
|
{hasPendingSchema ? (
|
|
277
311
|
<Card>
|
|
@@ -281,7 +315,7 @@ export function TaskProgressPage({
|
|
|
281
315
|
</Card>
|
|
282
316
|
) : null}
|
|
283
317
|
|
|
284
|
-
<div className="grid gap-
|
|
318
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
|
285
319
|
<SummaryCard
|
|
286
320
|
icon={<BarChart3 className="h-4 w-4" />}
|
|
287
321
|
label={t('summary.total')}
|
|
@@ -22,15 +22,17 @@ export function SummaryCard({
|
|
|
22
22
|
value: number;
|
|
23
23
|
}) {
|
|
24
24
|
return (
|
|
25
|
-
<Card>
|
|
25
|
+
<Card className="group overflow-hidden transition-all hover:-translate-y-0.5 hover:shadow-md">
|
|
26
26
|
<CardHeader className="pb-2">
|
|
27
27
|
<CardTitle className="flex items-center gap-2 text-muted-foreground text-sm">
|
|
28
|
-
|
|
28
|
+
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-dynamic-blue/10 text-dynamic-blue transition-transform group-hover:scale-110">
|
|
29
|
+
{icon}
|
|
30
|
+
</span>
|
|
29
31
|
{label}
|
|
30
32
|
</CardTitle>
|
|
31
33
|
</CardHeader>
|
|
32
34
|
<CardContent>
|
|
33
|
-
<div className="font-bold text-
|
|
35
|
+
<div className="font-bold text-3xl tracking-tight">
|
|
34
36
|
{Number(value).toLocaleString()}
|
|
35
37
|
</div>
|
|
36
38
|
</CardContent>
|
|
@@ -49,7 +51,7 @@ function MetricSelect({
|
|
|
49
51
|
}) {
|
|
50
52
|
return (
|
|
51
53
|
<select
|
|
52
|
-
className="h-
|
|
54
|
+
className="h-11 w-full rounded-xl border bg-background px-3 text-sm shadow-sm outline-none transition focus:border-dynamic-blue focus:ring-2 focus:ring-dynamic-blue/15"
|
|
53
55
|
defaultValue={selectedMetric?.id}
|
|
54
56
|
name={name}
|
|
55
57
|
required
|
|
@@ -81,8 +83,8 @@ export function ProgressPanel(props: {
|
|
|
81
83
|
} = props;
|
|
82
84
|
|
|
83
85
|
return (
|
|
84
|
-
<div className="grid gap-4
|
|
85
|
-
<Card>
|
|
86
|
+
<div className="grid gap-4 xl:grid-cols-[minmax(20rem,0.75fr)_minmax(0,1.25fr)]">
|
|
87
|
+
<Card className="h-fit xl:sticky xl:top-6">
|
|
86
88
|
<CardHeader>
|
|
87
89
|
<CardTitle>{t('progress.log_entry')}</CardTitle>
|
|
88
90
|
</CardHeader>
|
|
@@ -138,7 +140,7 @@ export function ProgressPanel(props: {
|
|
|
138
140
|
) : (
|
|
139
141
|
entries.map((entry) => (
|
|
140
142
|
<div
|
|
141
|
-
className="flex items-start justify-between gap-3 rounded-
|
|
143
|
+
className="flex items-start justify-between gap-3 rounded-xl border bg-background p-4 transition-colors hover:bg-muted/30"
|
|
142
144
|
key={entry.id}
|
|
143
145
|
>
|
|
144
146
|
<div>
|
|
@@ -177,8 +179,8 @@ export function GoalsPanel(props: {
|
|
|
177
179
|
const { createGoalMutation, goals, metrics, selectedMetric, t } = props;
|
|
178
180
|
|
|
179
181
|
return (
|
|
180
|
-
<div className="grid gap-4
|
|
181
|
-
<Card>
|
|
182
|
+
<div className="grid gap-4 xl:grid-cols-[minmax(20rem,0.75fr)_minmax(0,1.25fr)]">
|
|
183
|
+
<Card className="h-fit xl:sticky xl:top-6">
|
|
182
184
|
<CardHeader>
|
|
183
185
|
<CardTitle>{t('goals.create_goal')}</CardTitle>
|
|
184
186
|
</CardHeader>
|
|
@@ -202,7 +204,7 @@ export function GoalsPanel(props: {
|
|
|
202
204
|
<Input defaultValue={today()} name="period_start" type="date" />
|
|
203
205
|
<Input name="period_end" type="date" />
|
|
204
206
|
<select
|
|
205
|
-
className="h-
|
|
207
|
+
className="h-11 rounded-xl border bg-background px-3 text-sm shadow-sm outline-none focus:border-dynamic-blue focus:ring-2 focus:ring-dynamic-blue/15"
|
|
206
208
|
name="goal_type"
|
|
207
209
|
>
|
|
208
210
|
<option value="target">{t('goal_types.target')}</option>
|
|
@@ -30,6 +30,7 @@ interface ProjectGridCardProps {
|
|
|
30
30
|
onDelete: (projectId: string) => void;
|
|
31
31
|
onManageTasks: (project: TaskProject) => void;
|
|
32
32
|
onNavigate: (projectId: string) => void;
|
|
33
|
+
navigationMode?: 'dialog' | 'route';
|
|
33
34
|
isUpdating: boolean;
|
|
34
35
|
isDeleting: boolean;
|
|
35
36
|
isLinking: boolean;
|
|
@@ -43,6 +44,7 @@ export function ProjectGridCard({
|
|
|
43
44
|
onDelete,
|
|
44
45
|
onManageTasks,
|
|
45
46
|
onNavigate,
|
|
47
|
+
navigationMode = 'route',
|
|
46
48
|
isUpdating,
|
|
47
49
|
isDeleting,
|
|
48
50
|
isLinking,
|
|
@@ -132,15 +134,29 @@ export function ProjectGridCard({
|
|
|
132
134
|
<Link2 className="h-4 w-4" />
|
|
133
135
|
{t('manage')}
|
|
134
136
|
</Button>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
{navigationMode === 'route' ? (
|
|
138
|
+
<NextLink
|
|
139
|
+
href={`/${wsId}${tasksHref(`/projects/${project.id}`)}`}
|
|
140
|
+
onClick={(event) => event.stopPropagation()}
|
|
141
|
+
>
|
|
142
|
+
<Button size="sm" className="w-full gap-2">
|
|
143
|
+
<ExternalLink className="h-4 w-4" />
|
|
144
|
+
{t('open')}
|
|
145
|
+
</Button>
|
|
146
|
+
</NextLink>
|
|
147
|
+
) : (
|
|
148
|
+
<Button
|
|
149
|
+
size="sm"
|
|
150
|
+
className="w-full gap-2"
|
|
151
|
+
onClick={(event) => {
|
|
152
|
+
event.stopPropagation();
|
|
153
|
+
onNavigate(project.id);
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
140
156
|
<ExternalLink className="h-4 w-4" />
|
|
141
157
|
{t('open')}
|
|
142
158
|
</Button>
|
|
143
|
-
|
|
159
|
+
)}
|
|
144
160
|
</div>
|
|
145
161
|
</Card>
|
|
146
162
|
);
|
|
@@ -30,6 +30,7 @@ interface ProjectListItemProps {
|
|
|
30
30
|
onDelete: (projectId: string) => void;
|
|
31
31
|
onManageTasks: (project: TaskProject) => void;
|
|
32
32
|
onNavigate: (projectId: string) => void;
|
|
33
|
+
navigationMode?: 'dialog' | 'route';
|
|
33
34
|
isUpdating: boolean;
|
|
34
35
|
isDeleting: boolean;
|
|
35
36
|
isLinking: boolean;
|
|
@@ -43,6 +44,7 @@ export function ProjectListItem({
|
|
|
43
44
|
onDelete,
|
|
44
45
|
onManageTasks,
|
|
45
46
|
onNavigate,
|
|
47
|
+
navigationMode = 'route',
|
|
46
48
|
isUpdating,
|
|
47
49
|
isDeleting,
|
|
48
50
|
isLinking,
|
|
@@ -61,14 +63,28 @@ export function ProjectListItem({
|
|
|
61
63
|
<div className="min-w-0 space-y-3">
|
|
62
64
|
<div className="flex items-start justify-between gap-3">
|
|
63
65
|
<div className="min-w-0">
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
{navigationMode === 'route' ? (
|
|
67
|
+
<NextLink
|
|
68
|
+
href={`/${wsId}${tasksHref(`/projects/${project.id}`)}`}
|
|
69
|
+
className="inline-flex max-w-full items-center gap-2 font-semibold text-lg hover:text-dynamic-blue"
|
|
70
|
+
onClick={(event) => event.stopPropagation()}
|
|
71
|
+
>
|
|
72
|
+
<span className="truncate">{project.name}</span>
|
|
73
|
+
<ExternalLink className="h-4 w-4 shrink-0 opacity-0 transition-opacity group-hover:opacity-100" />
|
|
74
|
+
</NextLink>
|
|
75
|
+
) : (
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
className="inline-flex max-w-full items-center gap-2 text-left font-semibold text-lg hover:text-dynamic-blue"
|
|
79
|
+
onClick={(event) => {
|
|
80
|
+
event.stopPropagation();
|
|
81
|
+
onNavigate(project.id);
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<span className="truncate">{project.name}</span>
|
|
85
|
+
<ExternalLink className="h-4 w-4 shrink-0 opacity-0 transition-opacity group-hover:opacity-100" />
|
|
86
|
+
</button>
|
|
87
|
+
)}
|
|
72
88
|
<p className="mt-1 line-clamp-2 text-muted-foreground text-sm">
|
|
73
89
|
{project.description || t('no_description')}
|
|
74
90
|
</p>
|
package/src/components/ui/tu-do/projects/projectId/components/__tests__/tasks-tab-layout.test.tsx
CHANGED
|
@@ -160,4 +160,10 @@ describe('TasksTab layout', () => {
|
|
|
160
160
|
expect(captured.listViewProps.preserveTaskOrder).toBe(true);
|
|
161
161
|
expect(captured.listViewProps.searchQuery).toBe('project');
|
|
162
162
|
});
|
|
163
|
+
|
|
164
|
+
it('omits removed projects route back link in embedded mode', () => {
|
|
165
|
+
renderTasksTab({ embedded: true });
|
|
166
|
+
|
|
167
|
+
expect(captured.boardHeaderProps.backUrl).toBeUndefined();
|
|
168
|
+
});
|
|
163
169
|
});
|
|
@@ -38,6 +38,7 @@ interface TasksTabProps {
|
|
|
38
38
|
setListStatusFilter: (filter: ListStatusFilter) => void;
|
|
39
39
|
setShowLinkTaskDialog: (show: boolean) => void;
|
|
40
40
|
onTaskPartialUpdate: (taskId: string, partial: Partial<Task>) => void;
|
|
41
|
+
embedded?: boolean;
|
|
41
42
|
isMultiSelectMode: boolean;
|
|
42
43
|
setIsMultiSelectMode: (mode: boolean) => void;
|
|
43
44
|
}
|
|
@@ -58,6 +59,7 @@ export function TasksTab({
|
|
|
58
59
|
setListStatusFilter,
|
|
59
60
|
setShowLinkTaskDialog,
|
|
60
61
|
onTaskPartialUpdate,
|
|
62
|
+
embedded = false,
|
|
61
63
|
isMultiSelectMode,
|
|
62
64
|
setIsMultiSelectMode,
|
|
63
65
|
}: TasksTabProps) {
|
|
@@ -207,7 +209,7 @@ export function TasksTab({
|
|
|
207
209
|
listStatusFilter={listStatusFilter}
|
|
208
210
|
onListStatusFilterChange={setListStatusFilter}
|
|
209
211
|
isPersonalWorkspace={workspace.personal}
|
|
210
|
-
backUrl={`/${wsId}${tasksHref('/projects')}`}
|
|
212
|
+
backUrl={embedded ? undefined : `/${wsId}${tasksHref('/projects')}`}
|
|
211
213
|
hideActions={true}
|
|
212
214
|
isMultiSelectMode={isMultiSelectMode}
|
|
213
215
|
setIsMultiSelectMode={setIsMultiSelectMode}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useQuery } from '@tanstack/react-query';
|
|
4
|
+
import { Loader2 } from '@tuturuuu/icons';
|
|
4
5
|
import {
|
|
5
6
|
getWorkspaceTaskProject,
|
|
6
7
|
getWorkspaceTaskProjectTasks,
|
|
@@ -16,8 +17,9 @@ interface Props {
|
|
|
16
17
|
projectId: string;
|
|
17
18
|
currentUserId: string;
|
|
18
19
|
workspace: Workspace;
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
embedded?: boolean;
|
|
21
|
+
initialProject?: TaskProjectWithRelations;
|
|
22
|
+
initialProjectData?: { tasks: Task[]; documents: Task[]; lists: TaskList[] };
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export default function TaskProjectDetailPageClient({
|
|
@@ -25,6 +27,7 @@ export default function TaskProjectDetailPageClient({
|
|
|
25
27
|
projectId,
|
|
26
28
|
currentUserId,
|
|
27
29
|
workspace,
|
|
30
|
+
embedded = false,
|
|
28
31
|
initialProject,
|
|
29
32
|
initialProjectData,
|
|
30
33
|
}: Props) {
|
|
@@ -53,7 +56,11 @@ export default function TaskProjectDetailPageClient({
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
if (!project || !projectData) {
|
|
56
|
-
return
|
|
59
|
+
return (
|
|
60
|
+
<div className="flex min-h-[320px] items-center justify-center">
|
|
61
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
const lists: TaskList[] = (projectData.lists ?? []).map((list) => ({
|
|
@@ -79,6 +86,7 @@ export default function TaskProjectDetailPageClient({
|
|
|
79
86
|
documents={projectData.documents ?? []}
|
|
80
87
|
lists={lists}
|
|
81
88
|
currentUserId={currentUserId}
|
|
89
|
+
embedded={embedded}
|
|
82
90
|
wsId={wsId}
|
|
83
91
|
/>
|
|
84
92
|
);
|
|
@@ -38,6 +38,7 @@ export function TaskProjectDetail({
|
|
|
38
38
|
documents,
|
|
39
39
|
lists,
|
|
40
40
|
currentUserId,
|
|
41
|
+
embedded = false,
|
|
41
42
|
wsId,
|
|
42
43
|
}: TaskProjectDetailProps) {
|
|
43
44
|
const router = useRouter();
|
|
@@ -334,6 +335,7 @@ export function TaskProjectDetail({
|
|
|
334
335
|
setListStatusFilter={setListStatusFilter}
|
|
335
336
|
setShowLinkTaskDialog={taskLinking.setShowLinkTaskDialog}
|
|
336
337
|
onTaskPartialUpdate={handleTaskPartialUpdate}
|
|
338
|
+
embedded={embedded}
|
|
337
339
|
isMultiSelectMode={isMultiSelectMode}
|
|
338
340
|
setIsMultiSelectMode={setIsMultiSelectMode}
|
|
339
341
|
/>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useRouter } from 'next/navigation';
|
|
4
4
|
import { useCallback, useMemo, useState } from 'react';
|
|
5
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../dialog';
|
|
5
6
|
import { useTasksHref } from '../tasks-route-context';
|
|
6
7
|
import {
|
|
7
8
|
ProjectGridCard,
|
|
@@ -16,11 +17,15 @@ import {
|
|
|
16
17
|
ManageTasksDialog,
|
|
17
18
|
} from './dialogs';
|
|
18
19
|
import { useProjectFilters, useTaskProjects } from './hooks';
|
|
20
|
+
import TaskProjectDetailPageClient from './projectId/task-project-detail-page-client';
|
|
19
21
|
import type { TaskProject, TaskProjectsClientProps } from './types';
|
|
20
22
|
|
|
21
23
|
export function TaskProjectsClient({
|
|
24
|
+
currentUserId,
|
|
25
|
+
detailMode = 'route',
|
|
22
26
|
wsId,
|
|
23
27
|
initialProjects,
|
|
28
|
+
workspace,
|
|
24
29
|
}: TaskProjectsClientProps) {
|
|
25
30
|
const router = useRouter();
|
|
26
31
|
const tasksHref = useTasksHref();
|
|
@@ -34,6 +39,9 @@ export function TaskProjectsClient({
|
|
|
34
39
|
const [managingProject, setManagingProject] = useState<TaskProject | null>(
|
|
35
40
|
null
|
|
36
41
|
);
|
|
42
|
+
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
|
|
43
|
+
null
|
|
44
|
+
);
|
|
37
45
|
|
|
38
46
|
// Custom hooks
|
|
39
47
|
const taskProjects = useTaskProjects({
|
|
@@ -74,6 +82,11 @@ export function TaskProjectsClient({
|
|
|
74
82
|
),
|
|
75
83
|
[taskProjects.projects]
|
|
76
84
|
);
|
|
85
|
+
const selectedProject = selectedProjectId
|
|
86
|
+
? (taskProjects.projects.find(
|
|
87
|
+
(project) => project.id === selectedProjectId
|
|
88
|
+
) ?? null)
|
|
89
|
+
: null;
|
|
77
90
|
|
|
78
91
|
// Handlers
|
|
79
92
|
const handleEditProject = (project: TaskProject) => {
|
|
@@ -120,9 +133,14 @@ export function TaskProjectsClient({
|
|
|
120
133
|
|
|
121
134
|
const navigateToProject = useCallback(
|
|
122
135
|
(projectId: string) => {
|
|
136
|
+
if (detailMode === 'dialog' && workspace && currentUserId) {
|
|
137
|
+
setSelectedProjectId(projectId);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
123
141
|
router.push(`/${wsId}${tasksHref(`/projects/${projectId}`)}`);
|
|
124
142
|
},
|
|
125
|
-
[router,
|
|
143
|
+
[currentUserId, detailMode, router, tasksHref, workspace, wsId]
|
|
126
144
|
);
|
|
127
145
|
|
|
128
146
|
return (
|
|
@@ -168,6 +186,7 @@ export function TaskProjectsClient({
|
|
|
168
186
|
onDelete={taskProjects.deleteProject}
|
|
169
187
|
onManageTasks={handleOpenManageTasks}
|
|
170
188
|
onNavigate={navigateToProject}
|
|
189
|
+
navigationMode={detailMode}
|
|
171
190
|
isUpdating={taskProjects.isUpdating}
|
|
172
191
|
isDeleting={taskProjects.isDeleting}
|
|
173
192
|
isLinking={taskProjects.isLinking}
|
|
@@ -186,6 +205,7 @@ export function TaskProjectsClient({
|
|
|
186
205
|
onDelete={taskProjects.deleteProject}
|
|
187
206
|
onManageTasks={handleOpenManageTasks}
|
|
188
207
|
onNavigate={navigateToProject}
|
|
208
|
+
navigationMode={detailMode}
|
|
189
209
|
isUpdating={taskProjects.isUpdating}
|
|
190
210
|
isDeleting={taskProjects.isDeleting}
|
|
191
211
|
isLinking={taskProjects.isLinking}
|
|
@@ -221,6 +241,30 @@ export function TaskProjectsClient({
|
|
|
221
241
|
isLinking={taskProjects.isLinking}
|
|
222
242
|
isUnlinking={taskProjects.isUnlinking}
|
|
223
243
|
/>
|
|
244
|
+
|
|
245
|
+
<Dialog
|
|
246
|
+
open={Boolean(selectedProjectId)}
|
|
247
|
+
onOpenChange={(open) => {
|
|
248
|
+
if (!open) setSelectedProjectId(null);
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
<DialogContent className="max-h-[90vh] max-w-6xl overflow-y-auto">
|
|
252
|
+
<DialogHeader>
|
|
253
|
+
<DialogTitle className="sr-only">
|
|
254
|
+
{selectedProject?.name ?? ''}
|
|
255
|
+
</DialogTitle>
|
|
256
|
+
</DialogHeader>
|
|
257
|
+
{selectedProjectId && workspace && currentUserId && (
|
|
258
|
+
<TaskProjectDetailPageClient
|
|
259
|
+
currentUserId={currentUserId}
|
|
260
|
+
embedded
|
|
261
|
+
projectId={selectedProjectId}
|
|
262
|
+
workspace={workspace}
|
|
263
|
+
wsId={wsId}
|
|
264
|
+
/>
|
|
265
|
+
)}
|
|
266
|
+
</DialogContent>
|
|
267
|
+
</Dialog>
|
|
224
268
|
</>
|
|
225
269
|
);
|
|
226
270
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TaskProjectLinkedItem } from '@tuturuuu/internal-api/tasks';
|
|
2
|
-
import type { TaskProjectWithRelations } from '@tuturuuu/types';
|
|
2
|
+
import type { TaskProjectWithRelations, Workspace } from '@tuturuuu/types';
|
|
3
3
|
|
|
4
4
|
export type ProjectStatus =
|
|
5
5
|
| 'backlog'
|
|
@@ -30,6 +30,9 @@ export interface TaskProject extends TaskProjectWithRelations {
|
|
|
30
30
|
export interface TaskProjectsClientProps {
|
|
31
31
|
wsId: string;
|
|
32
32
|
initialProjects: TaskProject[];
|
|
33
|
+
currentUserId?: string;
|
|
34
|
+
detailMode?: 'dialog' | 'route';
|
|
35
|
+
workspace?: Workspace | null;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export interface TaskOption {
|
|
@@ -70,6 +70,10 @@ interface TaskDialogState {
|
|
|
70
70
|
taskWorkspaceTier?: WorkspaceProductTier;
|
|
71
71
|
/** Initial board/list context used for immediate partial-task rendering. */
|
|
72
72
|
initialSharedContext?: SharedTaskContext;
|
|
73
|
+
/** Board that supplied the visible task card before source-task hydration. */
|
|
74
|
+
visibleBoardId?: string;
|
|
75
|
+
/** Visible task snapshot before hydration, used to preserve overlay metadata. */
|
|
76
|
+
visibleTaskSnapshot?: Partial<Task>;
|
|
73
77
|
/** True while an existing task was opened from a partial snapshot and is hydrating. */
|
|
74
78
|
isHydratingTask?: boolean;
|
|
75
79
|
/** True when the latest hydration request failed after the dialog already opened. */
|
|
@@ -90,6 +94,8 @@ interface OpenTaskByIdOptions {
|
|
|
90
94
|
canUseBoardAssignees?: boolean;
|
|
91
95
|
assigneeMemberSource?: TaskAssigneeMemberSource;
|
|
92
96
|
initialSharedContext?: SharedTaskContext;
|
|
97
|
+
visibleBoardId?: string;
|
|
98
|
+
visibleTaskSnapshot?: Partial<Task>;
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
interface TaskDialogContextValue {
|
|
@@ -486,6 +492,8 @@ export function TaskDialogProvider({
|
|
|
486
492
|
assigneeMemberSource: options?.assigneeMemberSource,
|
|
487
493
|
taskWorkspaceTier: options?.taskWorkspaceTier,
|
|
488
494
|
initialSharedContext: options?.initialSharedContext,
|
|
495
|
+
visibleBoardId: options?.visibleBoardId,
|
|
496
|
+
visibleTaskSnapshot: options?.visibleTaskSnapshot,
|
|
489
497
|
isHydratingTask: true,
|
|
490
498
|
taskLoadError: false,
|
|
491
499
|
taskHydrationVersion: 0,
|
|
@@ -567,6 +575,8 @@ export function TaskDialogProvider({
|
|
|
567
575
|
options?.canUseBoardAssignees ?? !isTaskWorkspacePersonal,
|
|
568
576
|
assigneeMemberSource: options?.assigneeMemberSource,
|
|
569
577
|
taskWorkspaceTier,
|
|
578
|
+
visibleBoardId: options?.visibleBoardId,
|
|
579
|
+
visibleTaskSnapshot: options?.visibleTaskSnapshot,
|
|
570
580
|
isHydratingTask: false,
|
|
571
581
|
taskLoadError: false,
|
|
572
582
|
taskHydrationVersion: 1,
|