@tuturuuu/ui 0.6.2 → 0.8.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 +66 -0
- package/biome.json +1 -1
- package/package.json +11 -11
- package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
- package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
- package/src/components/ui/calendar.test.tsx +24 -0
- package/src/components/ui/calendar.tsx +1 -0
- package/src/components/ui/currency-input.test.tsx +43 -0
- package/src/components/ui/currency-input.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
- package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
- package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
- package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
- package/src/components/ui/date-time-picker.tsx +352 -234
- package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
- package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
- package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
- package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
- package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
- package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
- package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
- package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
- package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
- package/src/components/ui/finance/transactions/form-types.ts +5 -0
- package/src/components/ui/finance/transactions/form.test.tsx +105 -22
- package/src/components/ui/finance/transactions/form.tsx +116 -20
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
- package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
- package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
- package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
- package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
- package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
- package/src/components/ui/money-input.test.tsx +64 -0
- package/src/components/ui/money-input.tsx +63 -0
- package/src/components/ui/optional-time-picker.tsx +95 -0
- package/src/components/ui/quick-command-center.test.tsx +90 -0
- package/src/components/ui/quick-command-center.tsx +190 -0
- package/src/components/ui/storefront/cart-summary.tsx +126 -50
- package/src/components/ui/storefront/checkout-overlay.tsx +27 -0
- package/src/components/ui/storefront/hero-panel.tsx +23 -20
- package/src/components/ui/storefront/image-panel.tsx +6 -0
- package/src/components/ui/storefront/index.ts +11 -0
- package/src/components/ui/storefront/listing-card.tsx +84 -22
- package/src/components/ui/storefront/product-detail.tsx +289 -0
- package/src/components/ui/storefront/product-dialog.tsx +72 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +132 -5
- package/src/components/ui/storefront/storefront-surface.tsx +371 -128
- package/src/components/ui/storefront/types.ts +25 -1
- package/src/components/ui/storefront/utils.ts +118 -13
- package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
- package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
- package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
- package/src/components/ui/text-editor/content-migration.ts +41 -18
- package/src/components/ui/text-editor/extensions.ts +1 -1
- package/src/components/ui/text-editor/image-extension.ts +40 -18
- package/src/components/ui/text-editor/video-extension.ts +11 -2
- package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
- package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +1 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +13 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +46 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +13 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +3 -1
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +56 -2
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
- package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +34 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +39 -0
- package/src/components/ui/tu-do/shared/board-views.tsx +9 -7
- package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
- package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +42 -14
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
- package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
- package/src/hooks/useBoardRealtime.ts +6 -3
- package/src/hooks/useBoardRealtime.types.ts +11 -0
- package/src/hooks/useCursorTracking.ts +91 -27
- package/src/hooks/useTaskUserRealtime.ts +5 -3
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveTaskDialogOpeningPresentation } from './task-dialog-presentation';
|
|
3
|
+
|
|
4
|
+
describe('resolveTaskDialogOpeningPresentation', () => {
|
|
5
|
+
it('opens existing document-list tasks fullscreen', () => {
|
|
6
|
+
expect(
|
|
7
|
+
resolveTaskDialogOpeningPresentation({
|
|
8
|
+
defaultPresentation: 'compact',
|
|
9
|
+
mode: 'edit',
|
|
10
|
+
selectedListStatus: 'documents',
|
|
11
|
+
})
|
|
12
|
+
).toBe('fullscreen');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('keeps create mode compact even in document lists', () => {
|
|
16
|
+
expect(
|
|
17
|
+
resolveTaskDialogOpeningPresentation({
|
|
18
|
+
defaultPresentation: 'fullscreen',
|
|
19
|
+
mode: 'create',
|
|
20
|
+
selectedListStatus: 'documents',
|
|
21
|
+
})
|
|
22
|
+
).toBe('compact');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('respects the user default for existing non-document tasks', () => {
|
|
26
|
+
expect(
|
|
27
|
+
resolveTaskDialogOpeningPresentation({
|
|
28
|
+
defaultPresentation: 'compact',
|
|
29
|
+
mode: 'edit',
|
|
30
|
+
selectedListStatus: 'active',
|
|
31
|
+
})
|
|
32
|
+
).toBe('compact');
|
|
33
|
+
|
|
34
|
+
expect(
|
|
35
|
+
resolveTaskDialogOpeningPresentation({
|
|
36
|
+
defaultPresentation: 'fullscreen',
|
|
37
|
+
mode: 'edit',
|
|
38
|
+
selectedListStatus: 'not_started',
|
|
39
|
+
})
|
|
40
|
+
).toBe('fullscreen');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('keeps drafts fullscreen', () => {
|
|
44
|
+
expect(
|
|
45
|
+
resolveTaskDialogOpeningPresentation({
|
|
46
|
+
defaultPresentation: 'compact',
|
|
47
|
+
draftId: 'draft-1',
|
|
48
|
+
mode: 'create',
|
|
49
|
+
selectedListStatus: 'documents',
|
|
50
|
+
})
|
|
51
|
+
).toBe('fullscreen');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -2,6 +2,7 @@ export const TASK_DIALOG_DEFAULT_PRESENTATION_CONFIG_ID =
|
|
|
2
2
|
'TASK_DIALOG_DEFAULT_PRESENTATION';
|
|
3
3
|
|
|
4
4
|
export type TaskDialogPresentation = 'compact' | 'fullscreen';
|
|
5
|
+
export type TaskDialogMode = 'edit' | 'create';
|
|
5
6
|
|
|
6
7
|
export function normalizeTaskDialogPresentation(
|
|
7
8
|
value: unknown,
|
|
@@ -9,3 +10,21 @@ export function normalizeTaskDialogPresentation(
|
|
|
9
10
|
): TaskDialogPresentation {
|
|
10
11
|
return value === 'fullscreen' || value === 'compact' ? value : fallback;
|
|
11
12
|
}
|
|
13
|
+
|
|
14
|
+
export function resolveTaskDialogOpeningPresentation({
|
|
15
|
+
defaultPresentation,
|
|
16
|
+
draftId,
|
|
17
|
+
mode = 'edit',
|
|
18
|
+
selectedListStatus,
|
|
19
|
+
}: {
|
|
20
|
+
defaultPresentation?: unknown;
|
|
21
|
+
draftId?: string;
|
|
22
|
+
mode?: TaskDialogMode;
|
|
23
|
+
selectedListStatus?: string | null;
|
|
24
|
+
}): TaskDialogPresentation {
|
|
25
|
+
if (draftId) return 'fullscreen';
|
|
26
|
+
if (mode === 'create') return 'compact';
|
|
27
|
+
if (selectedListStatus === 'documents') return 'fullscreen';
|
|
28
|
+
|
|
29
|
+
return normalizeTaskDialogPresentation(defaultPresentation);
|
|
30
|
+
}
|
|
@@ -178,6 +178,63 @@ describe('CompactTaskCreatePopover', () => {
|
|
|
178
178
|
).not.toBeInTheDocument();
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
it('renders compact description preview without affecting panel layout', () => {
|
|
182
|
+
const onDescriptionPreviewClick = vi.fn();
|
|
183
|
+
|
|
184
|
+
render(
|
|
185
|
+
<Dialog open={true}>
|
|
186
|
+
<CompactTaskCreatePopover
|
|
187
|
+
title="Edit task"
|
|
188
|
+
titleInput={<input aria-label="Task title" defaultValue="Existing" />}
|
|
189
|
+
propertyControls={
|
|
190
|
+
<button type="button" aria-label="List: Inbox">
|
|
191
|
+
List
|
|
192
|
+
</button>
|
|
193
|
+
}
|
|
194
|
+
descriptionPreview="Confirm the plan and publish the final notes."
|
|
195
|
+
descriptionPreviewLabel="Open full task"
|
|
196
|
+
onDescriptionPreviewClick={onDescriptionPreviewClick}
|
|
197
|
+
onClose={vi.fn()}
|
|
198
|
+
onFullscreen={vi.fn()}
|
|
199
|
+
/>
|
|
200
|
+
</Dialog>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const preview = screen.getByTestId('compact-task-description-preview');
|
|
204
|
+
|
|
205
|
+
expect(preview).toHaveTextContent(
|
|
206
|
+
'Confirm the plan and publish the final notes.'
|
|
207
|
+
);
|
|
208
|
+
expect(preview).toHaveAttribute('aria-label', 'Open full task');
|
|
209
|
+
expect(preview).toHaveClass('absolute', 'top-full');
|
|
210
|
+
|
|
211
|
+
fireEvent.click(preview);
|
|
212
|
+
|
|
213
|
+
expect(onDescriptionPreviewClick).toHaveBeenCalledTimes(1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('omits compact description preview when the caller does not provide one', () => {
|
|
217
|
+
render(
|
|
218
|
+
<Dialog open={true}>
|
|
219
|
+
<CompactTaskCreatePopover
|
|
220
|
+
title="Create task"
|
|
221
|
+
titleInput={<input aria-label="Task title" defaultValue="New" />}
|
|
222
|
+
propertyControls={
|
|
223
|
+
<button type="button" aria-label="List: Inbox">
|
|
224
|
+
List
|
|
225
|
+
</button>
|
|
226
|
+
}
|
|
227
|
+
onClose={vi.fn()}
|
|
228
|
+
onFullscreen={vi.fn()}
|
|
229
|
+
/>
|
|
230
|
+
</Dialog>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(
|
|
234
|
+
screen.queryByTestId('compact-task-description-preview')
|
|
235
|
+
).not.toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
|
|
181
238
|
it('renders compact edit actions when provided', () => {
|
|
182
239
|
const onDelete = vi.fn();
|
|
183
240
|
const onDone = vi.fn();
|
package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx
CHANGED
|
@@ -25,6 +25,8 @@ interface CompactTaskDialogPanelProps {
|
|
|
25
25
|
iconRingClass?: string;
|
|
26
26
|
titleInput: ReactNode;
|
|
27
27
|
showHeaderTitle?: boolean;
|
|
28
|
+
descriptionPreview?: string | null;
|
|
29
|
+
descriptionPreviewLabel?: string;
|
|
28
30
|
taskStatus?: ReactNode;
|
|
29
31
|
propertyControls: ReactNode;
|
|
30
32
|
editActions?: ReactNode;
|
|
@@ -39,6 +41,7 @@ interface CompactTaskDialogPanelProps {
|
|
|
39
41
|
onCreateMultipleChange?: (value: boolean) => void;
|
|
40
42
|
onClose: () => void;
|
|
41
43
|
onFullscreen: () => void;
|
|
44
|
+
onDescriptionPreviewClick?: () => void;
|
|
42
45
|
onSave?: () => void;
|
|
43
46
|
}
|
|
44
47
|
|
|
@@ -84,6 +87,8 @@ export function CompactTaskDialogPanel({
|
|
|
84
87
|
iconRingClass = 'ring-dynamic-orange/20',
|
|
85
88
|
titleInput,
|
|
86
89
|
showHeaderTitle = true,
|
|
90
|
+
descriptionPreview,
|
|
91
|
+
descriptionPreviewLabel,
|
|
87
92
|
taskStatus,
|
|
88
93
|
propertyControls,
|
|
89
94
|
editActions,
|
|
@@ -98,6 +103,7 @@ export function CompactTaskDialogPanel({
|
|
|
98
103
|
onCreateMultipleChange,
|
|
99
104
|
onClose,
|
|
100
105
|
onFullscreen,
|
|
106
|
+
onDescriptionPreviewClick,
|
|
101
107
|
onSave,
|
|
102
108
|
}: CompactTaskDialogPanelProps) {
|
|
103
109
|
const t = useTranslations();
|
|
@@ -114,127 +120,146 @@ export function CompactTaskDialogPanel({
|
|
|
114
120
|
const hasHeaderTitle = showHeaderTitle;
|
|
115
121
|
|
|
116
122
|
return (
|
|
117
|
-
<div
|
|
118
|
-
data-testid="compact-task-dialog-panel"
|
|
119
|
-
className="flex max-h-[calc(100vh-2rem)] min-h-0 flex-col overflow-hidden rounded-lg bg-background"
|
|
120
|
-
>
|
|
123
|
+
<div className="relative">
|
|
121
124
|
<div
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
hasHeaderTitle ? 'justify-between' : 'justify-end'
|
|
125
|
-
)}
|
|
125
|
+
data-testid="compact-task-dialog-panel"
|
|
126
|
+
className="flex max-h-[calc(100vh-2rem)] min-h-0 flex-col overflow-hidden rounded-lg bg-background"
|
|
126
127
|
>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
128
|
+
<div
|
|
129
|
+
className={cn(
|
|
130
|
+
'flex items-start gap-3 border-b px-4 py-3',
|
|
131
|
+
hasHeaderTitle ? 'justify-between' : 'justify-end'
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
{hasHeaderTitle ? (
|
|
135
|
+
<div className="flex min-w-0 items-start gap-2.5">
|
|
136
|
+
<div
|
|
137
|
+
className={cn(
|
|
138
|
+
'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ring-1',
|
|
139
|
+
iconBgClass,
|
|
140
|
+
iconRingClass
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
{icon ?? <ListTodo className="h-4 w-4 text-dynamic-orange" />}
|
|
144
|
+
</div>
|
|
145
|
+
<div className="min-w-0 space-y-0.5">
|
|
146
|
+
<DialogTitle className="truncate font-semibold text-base">
|
|
147
|
+
{title}
|
|
148
|
+
</DialogTitle>
|
|
149
|
+
{description && (
|
|
150
|
+
<DialogDescription className="truncate text-muted-foreground text-xs">
|
|
151
|
+
{description}
|
|
152
|
+
</DialogDescription>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
147
155
|
</div>
|
|
156
|
+
) : (
|
|
157
|
+
<DialogTitle className="sr-only">{title}</DialogTitle>
|
|
158
|
+
)}
|
|
159
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
160
|
+
{smartAction}
|
|
161
|
+
{editActions}
|
|
162
|
+
<Tooltip>
|
|
163
|
+
<TooltipTrigger asChild>
|
|
164
|
+
<Button
|
|
165
|
+
type="button"
|
|
166
|
+
variant="ghost"
|
|
167
|
+
size="icon"
|
|
168
|
+
aria-label={t('ws-task-boards.dialog.open_fullscreen')}
|
|
169
|
+
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
170
|
+
onClick={onFullscreen}
|
|
171
|
+
>
|
|
172
|
+
<Maximize2 className="h-4 w-4" />
|
|
173
|
+
</Button>
|
|
174
|
+
</TooltipTrigger>
|
|
175
|
+
<TooltipContent side="bottom">
|
|
176
|
+
{t('ws-task-boards.dialog.open_fullscreen')}
|
|
177
|
+
</TooltipContent>
|
|
178
|
+
</Tooltip>
|
|
179
|
+
<Tooltip>
|
|
180
|
+
<TooltipTrigger asChild>
|
|
181
|
+
<Button
|
|
182
|
+
type="button"
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="icon"
|
|
185
|
+
aria-label={t('common.close')}
|
|
186
|
+
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
187
|
+
onClick={onClose}
|
|
188
|
+
>
|
|
189
|
+
<X className="h-4 w-4" />
|
|
190
|
+
</Button>
|
|
191
|
+
</TooltipTrigger>
|
|
192
|
+
<TooltipContent side="bottom">{t('common.close')}</TooltipContent>
|
|
193
|
+
</Tooltip>
|
|
148
194
|
</div>
|
|
149
|
-
) : (
|
|
150
|
-
<DialogTitle className="sr-only">{title}</DialogTitle>
|
|
151
|
-
)}
|
|
152
|
-
<div className="flex shrink-0 items-center gap-1">
|
|
153
|
-
{smartAction}
|
|
154
|
-
{editActions}
|
|
155
|
-
<Tooltip>
|
|
156
|
-
<TooltipTrigger asChild>
|
|
157
|
-
<Button
|
|
158
|
-
type="button"
|
|
159
|
-
variant="ghost"
|
|
160
|
-
size="icon"
|
|
161
|
-
aria-label={t('ws-task-boards.dialog.open_fullscreen')}
|
|
162
|
-
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
163
|
-
onClick={onFullscreen}
|
|
164
|
-
>
|
|
165
|
-
<Maximize2 className="h-4 w-4" />
|
|
166
|
-
</Button>
|
|
167
|
-
</TooltipTrigger>
|
|
168
|
-
<TooltipContent side="bottom">
|
|
169
|
-
{t('ws-task-boards.dialog.open_fullscreen')}
|
|
170
|
-
</TooltipContent>
|
|
171
|
-
</Tooltip>
|
|
172
|
-
<Tooltip>
|
|
173
|
-
<TooltipTrigger asChild>
|
|
174
|
-
<Button
|
|
175
|
-
type="button"
|
|
176
|
-
variant="ghost"
|
|
177
|
-
size="icon"
|
|
178
|
-
aria-label={t('common.close')}
|
|
179
|
-
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
180
|
-
onClick={onClose}
|
|
181
|
-
>
|
|
182
|
-
<X className="h-4 w-4" />
|
|
183
|
-
</Button>
|
|
184
|
-
</TooltipTrigger>
|
|
185
|
-
<TooltipContent side="bottom">{t('common.close')}</TooltipContent>
|
|
186
|
-
</Tooltip>
|
|
187
195
|
</div>
|
|
188
|
-
</div>
|
|
189
196
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
<div className="min-h-0 space-y-3 overflow-y-auto px-4 py-3">
|
|
198
|
+
{titleInput}
|
|
199
|
+
{taskStatus}
|
|
200
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
201
|
+
{propertyControls}
|
|
202
|
+
</div>
|
|
203
|
+
{smartPanel}
|
|
195
204
|
</div>
|
|
196
|
-
{smartPanel}
|
|
197
|
-
</div>
|
|
198
205
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
206
|
+
{hasCreateActions && (
|
|
207
|
+
<div className="flex items-center justify-between gap-2 border-t bg-muted/20 px-4 py-3">
|
|
208
|
+
<div className="flex items-center gap-1">
|
|
209
|
+
<CompactIconButton
|
|
210
|
+
active={!!saveAsDraft}
|
|
211
|
+
label={t('task-drafts.save_as_draft')}
|
|
212
|
+
onClick={() => onSaveAsDraftChange?.(!saveAsDraft)}
|
|
213
|
+
>
|
|
214
|
+
<FileEdit className="h-4 w-4" />
|
|
215
|
+
</CompactIconButton>
|
|
216
|
+
<CompactIconButton
|
|
217
|
+
active={!!createMultiple}
|
|
218
|
+
label={t('ws-task-boards.dialog.create_multiple')}
|
|
219
|
+
onClick={() => onCreateMultipleChange?.(!createMultiple)}
|
|
220
|
+
>
|
|
221
|
+
<Copy className="h-4 w-4" />
|
|
222
|
+
</CompactIconButton>
|
|
223
|
+
<QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
|
|
224
|
+
</div>
|
|
225
|
+
<Button
|
|
226
|
+
type="button"
|
|
227
|
+
size="sm"
|
|
228
|
+
disabled={!canSave}
|
|
229
|
+
onClick={() => onSave?.()}
|
|
230
|
+
className="min-w-28"
|
|
213
231
|
>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
232
|
+
{isLoading ? (
|
|
233
|
+
<>
|
|
234
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
235
|
+
{t('ws-task-boards.dialog.saving')}
|
|
236
|
+
</>
|
|
237
|
+
) : (
|
|
238
|
+
<>
|
|
239
|
+
<Check className="h-4 w-4" />
|
|
240
|
+
{saveLabel}
|
|
241
|
+
</>
|
|
242
|
+
)}
|
|
243
|
+
</Button>
|
|
217
244
|
</div>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
</Button>
|
|
237
|
-
</div>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{descriptionPreview && onDescriptionPreviewClick && (
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
data-testid="compact-task-description-preview"
|
|
252
|
+
aria-label={
|
|
253
|
+
descriptionPreviewLabel ??
|
|
254
|
+
t('ws-task-boards.dialog.open_fullscreen')
|
|
255
|
+
}
|
|
256
|
+
className="absolute top-full left-1/2 mt-2 w-full max-w-[30rem] -translate-x-1/2 rounded-lg border bg-background/95 px-4 py-3 text-left opacity-70 shadow-xl ring-1 ring-border/60 backdrop-blur transition hover:bg-muted/70 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
257
|
+
onClick={onDescriptionPreviewClick}
|
|
258
|
+
>
|
|
259
|
+
<span className="line-clamp-3 whitespace-pre-line text-muted-foreground text-sm leading-relaxed">
|
|
260
|
+
{descriptionPreview}
|
|
261
|
+
</span>
|
|
262
|
+
</button>
|
|
238
263
|
)}
|
|
239
264
|
</div>
|
|
240
265
|
);
|
package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type { QueryClient } from '@tanstack/react-query';
|
|
|
4
4
|
import type { Editor, JSONContent } from '@tiptap/react';
|
|
5
5
|
import { Loader2 } from '@tuturuuu/icons';
|
|
6
6
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
7
|
+
import { getBoardRealtimeChannelName } from '@tuturuuu/ui/hooks/useBoardRealtime.types';
|
|
7
8
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
8
9
|
import { cn } from '@tuturuuu/utils/format';
|
|
9
10
|
import { useTranslations } from 'next-intl';
|
|
@@ -352,8 +353,9 @@ export function TaskDescriptionEditor({
|
|
|
352
353
|
|
|
353
354
|
{showCollaborationCursors && taskId && (
|
|
354
355
|
<CursorOverlayMultiWrapper
|
|
355
|
-
channelName={
|
|
356
|
+
channelName={getBoardRealtimeChannelName(boardId)}
|
|
356
357
|
containerRef={richTextEditorRef}
|
|
358
|
+
cursorScope={{ taskId, type: 'task-description' }}
|
|
357
359
|
/>
|
|
358
360
|
)}
|
|
359
361
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const internalApiMocks = vi.hoisted(() => ({
|
|
4
|
+
abortWorkspaceTaskDescriptionChunks: vi.fn(),
|
|
5
|
+
appendWorkspaceTaskDescriptionChunk: vi.fn(),
|
|
6
|
+
beginWorkspaceTaskDescriptionChunks: vi.fn(),
|
|
7
|
+
commitWorkspaceTaskDescriptionChunks: vi.fn(),
|
|
8
|
+
createWorkspaceTaskProject: vi.fn(),
|
|
9
|
+
getWorkspaceTask: vi.fn(),
|
|
10
|
+
getWorkspaceTaskDescription: vi.fn(),
|
|
11
|
+
updateWorkspaceTask: vi.fn(),
|
|
12
|
+
updateWorkspaceTaskDescription: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('@tuturuuu/internal-api/tasks', () => internalApiMocks);
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
shouldChunkTaskDescriptionPayload,
|
|
19
|
+
updateWorkspaceTaskDescription,
|
|
20
|
+
} from './task-api';
|
|
21
|
+
|
|
22
|
+
describe('task-api description persistence', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.resetAllMocks();
|
|
25
|
+
internalApiMocks.abortWorkspaceTaskDescriptionChunks.mockResolvedValue({
|
|
26
|
+
success: true,
|
|
27
|
+
});
|
|
28
|
+
internalApiMocks.appendWorkspaceTaskDescriptionChunk.mockResolvedValue({
|
|
29
|
+
success: true,
|
|
30
|
+
});
|
|
31
|
+
internalApiMocks.beginWorkspaceTaskDescriptionChunks.mockResolvedValue({
|
|
32
|
+
session_id: 'chunk-session-1',
|
|
33
|
+
});
|
|
34
|
+
internalApiMocks.commitWorkspaceTaskDescriptionChunks.mockResolvedValue({
|
|
35
|
+
description: 'persisted',
|
|
36
|
+
description_yjs_state: [1, 2, 3],
|
|
37
|
+
});
|
|
38
|
+
internalApiMocks.updateWorkspaceTaskDescription.mockResolvedValue({
|
|
39
|
+
description: 'small',
|
|
40
|
+
description_yjs_state: [1, 2, 3],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('uses the direct description update for small payloads', async () => {
|
|
45
|
+
const payload = {
|
|
46
|
+
description: JSON.stringify({
|
|
47
|
+
type: 'doc',
|
|
48
|
+
content: [{ type: 'paragraph', content: [{ text: 'Small' }] }],
|
|
49
|
+
}),
|
|
50
|
+
description_yjs_state: [1, 2, 3],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
await expect(
|
|
54
|
+
updateWorkspaceTaskDescription('ws-1', 'task-1', payload)
|
|
55
|
+
).resolves.toEqual({
|
|
56
|
+
description: 'small',
|
|
57
|
+
description_yjs_state: [1, 2, 3],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(
|
|
61
|
+
internalApiMocks.updateWorkspaceTaskDescription
|
|
62
|
+
).toHaveBeenCalledWith('ws-1', 'task-1', payload);
|
|
63
|
+
expect(
|
|
64
|
+
internalApiMocks.beginWorkspaceTaskDescriptionChunks
|
|
65
|
+
).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('uploads large description updates through ordered chunks', async () => {
|
|
69
|
+
const yjsState = Array.from({ length: 220_000 }, (_, index) => index % 256);
|
|
70
|
+
const payload = {
|
|
71
|
+
description: JSON.stringify({
|
|
72
|
+
type: 'doc',
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: 'paragraph',
|
|
76
|
+
content: [{ type: 'text', text: 'Large paste' }],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
}),
|
|
80
|
+
description_yjs_state: yjsState,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
expect(shouldChunkTaskDescriptionPayload(payload)).toBe(true);
|
|
84
|
+
|
|
85
|
+
await updateWorkspaceTaskDescription('ws-1', 'task-1', payload);
|
|
86
|
+
|
|
87
|
+
expect(
|
|
88
|
+
internalApiMocks.updateWorkspaceTaskDescription
|
|
89
|
+
).not.toHaveBeenCalled();
|
|
90
|
+
expect(
|
|
91
|
+
internalApiMocks.beginWorkspaceTaskDescriptionChunks
|
|
92
|
+
).toHaveBeenCalledWith(
|
|
93
|
+
'ws-1',
|
|
94
|
+
'task-1',
|
|
95
|
+
expect.objectContaining({
|
|
96
|
+
description: expect.objectContaining({
|
|
97
|
+
chunk_count: 1,
|
|
98
|
+
}),
|
|
99
|
+
description_yjs_state: expect.objectContaining({
|
|
100
|
+
chunk_count: expect.any(Number),
|
|
101
|
+
}),
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const appendCalls =
|
|
106
|
+
internalApiMocks.appendWorkspaceTaskDescriptionChunk.mock.calls;
|
|
107
|
+
expect(appendCalls.length).toBeGreaterThan(2);
|
|
108
|
+
expect(appendCalls[0]).toEqual([
|
|
109
|
+
'ws-1',
|
|
110
|
+
'task-1',
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
chunk_index: 0,
|
|
113
|
+
field: 'description',
|
|
114
|
+
session_id: 'chunk-session-1',
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
117
|
+
expect(
|
|
118
|
+
appendCalls
|
|
119
|
+
.filter((call) => call[2].field === 'description_yjs_state')
|
|
120
|
+
.map((call) => call[2].chunk_index)
|
|
121
|
+
).toEqual(
|
|
122
|
+
appendCalls
|
|
123
|
+
.filter((call) => call[2].field === 'description_yjs_state')
|
|
124
|
+
.map((_, index) => index)
|
|
125
|
+
);
|
|
126
|
+
expect(
|
|
127
|
+
internalApiMocks.commitWorkspaceTaskDescriptionChunks
|
|
128
|
+
).toHaveBeenCalledWith('ws-1', 'task-1', 'chunk-session-1');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('falls back to chunked upload when a direct save hits the proxy body limit', async () => {
|
|
132
|
+
internalApiMocks.updateWorkspaceTaskDescription.mockRejectedValueOnce({
|
|
133
|
+
status: 413,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await updateWorkspaceTaskDescription('ws-1', 'task-1', {
|
|
137
|
+
description: 'small enough to try directly',
|
|
138
|
+
description_yjs_state: [1, 2, 3],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(
|
|
142
|
+
internalApiMocks.beginWorkspaceTaskDescriptionChunks
|
|
143
|
+
).toHaveBeenCalled();
|
|
144
|
+
expect(
|
|
145
|
+
internalApiMocks.commitWorkspaceTaskDescriptionChunks
|
|
146
|
+
).toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('aborts the chunk session when an append fails', async () => {
|
|
150
|
+
internalApiMocks.appendWorkspaceTaskDescriptionChunk.mockRejectedValueOnce(
|
|
151
|
+
new Error('network down')
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await expect(
|
|
155
|
+
updateWorkspaceTaskDescription('ws-1', 'task-1', {
|
|
156
|
+
description: 'x'.repeat(10),
|
|
157
|
+
description_yjs_state: Array.from(
|
|
158
|
+
{ length: 220_000 },
|
|
159
|
+
(_, index) => index % 256
|
|
160
|
+
),
|
|
161
|
+
})
|
|
162
|
+
).rejects.toThrow('network down');
|
|
163
|
+
|
|
164
|
+
expect(
|
|
165
|
+
internalApiMocks.abortWorkspaceTaskDescriptionChunks
|
|
166
|
+
).toHaveBeenCalledWith('ws-1', 'task-1', 'chunk-session-1');
|
|
167
|
+
expect(
|
|
168
|
+
internalApiMocks.commitWorkspaceTaskDescriptionChunks
|
|
169
|
+
).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
});
|