@tuturuuu/ui 0.10.0 → 0.11.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/finance/invoices/components/subscription-attendance-summary.tsx +19 -5
- package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.test.tsx +41 -0
- package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.tsx +93 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +82 -70
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +50 -25
- package/src/components/ui/finance/invoices/hooks.ts +11 -2
- package/src/components/ui/finance/invoices/internal-api.ts +5 -0
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +92 -31
- package/src/components/ui/finance/invoices/utils.test.ts +82 -0
- package/src/components/ui/finance/invoices/utils.ts +240 -7
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -0
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +37 -0
- package/src/components/ui/tu-do/shared/board-client.tsx +3 -1
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +55 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.test.ts +97 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.ts +210 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +56 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.tsx +218 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-restore-banner.tsx +83 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.test.tsx +120 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.tsx +180 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/utils.test.ts +100 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/utils.ts +109 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +381 -34
- package/src/components/ui/tu-do/templates/task-template-api.ts +142 -0
- package/src/components/ui/tu-do/templates/task-template-card.tsx +118 -0
- package/src/components/ui/tu-do/templates/task-template-client.tsx +258 -0
- package/src/components/ui/tu-do/templates/task-template-dialogs.test.tsx +167 -0
- package/src/components/ui/tu-do/templates/task-template-dialogs.tsx +376 -0
- package/src/components/ui/tu-do/templates/task-templates-hub.test.tsx +114 -0
- package/src/components/ui/tu-do/templates/task-templates-hub.tsx +50 -0
- package/src/components/ui/tu-do/templates/task-templates-page.tsx +6 -11
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
uploadWorkspaceTaskFile,
|
|
12
12
|
} from '@tuturuuu/internal-api';
|
|
13
13
|
import type { WorkspaceTaskSuggestionTask } from '@tuturuuu/internal-api/tasks';
|
|
14
|
+
import { getWorkspaceTaskHistory } from '@tuturuuu/internal-api/tasks';
|
|
14
15
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
15
16
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
16
17
|
import { Button } from '@tuturuuu/ui/button';
|
|
17
18
|
import { Dialog, DialogContent, DialogTitle } from '@tuturuuu/ui/dialog';
|
|
19
|
+
import { SUPABASE_PROVIDER_SYNC_ORIGIN } from '@tuturuuu/ui/hooks/supabase-provider';
|
|
18
20
|
import { useYjsCollaboration } from '@tuturuuu/ui/hooks/use-yjs-collaboration';
|
|
19
21
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
20
22
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
|
|
@@ -30,8 +32,9 @@ import {
|
|
|
30
32
|
} from '@tuturuuu/utils/yjs-helper';
|
|
31
33
|
import dayjs from 'dayjs';
|
|
32
34
|
import { usePathname } from 'next/navigation';
|
|
33
|
-
import { useTranslations } from 'next-intl';
|
|
35
|
+
import { useLocale, useTranslations } from 'next-intl';
|
|
34
36
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
37
|
+
import * as Y from 'yjs';
|
|
35
38
|
import { BoardEstimationConfigDialog } from '../boards/boardId/task-dialogs/BoardEstimationConfigDialog';
|
|
36
39
|
import { TaskNewLabelDialog } from '../boards/boardId/task-dialogs/TaskNewLabelDialog';
|
|
37
40
|
import { TaskNewProjectDialog } from '../boards/boardId/task-dialogs/TaskNewProjectDialog';
|
|
@@ -64,7 +67,14 @@ import {
|
|
|
64
67
|
import { TaskNameInput } from './task-edit-dialog/components/task-name-input';
|
|
65
68
|
import { TaskSuggestionMenus } from './task-edit-dialog/components/task-suggestion-menus';
|
|
66
69
|
import { NAME_UPDATE_DEBOUNCE_MS } from './task-edit-dialog/constants';
|
|
67
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
buildRecoverableTaskDescriptionVersions,
|
|
72
|
+
type RecoverableTaskDescriptionVersion,
|
|
73
|
+
} from './task-edit-dialog/description-versions';
|
|
74
|
+
import {
|
|
75
|
+
fetchWorkspaceTaskDescription,
|
|
76
|
+
updateWorkspaceTaskDescription,
|
|
77
|
+
} from './task-edit-dialog/hooks/task-api';
|
|
68
78
|
import { useEditorCommands } from './task-edit-dialog/hooks/use-editor-commands';
|
|
69
79
|
import { useSuggestionMenus } from './task-edit-dialog/hooks/use-suggestion-menus';
|
|
70
80
|
import { useTaskChangeDetection } from './task-edit-dialog/hooks/use-task-change-detection';
|
|
@@ -85,6 +95,8 @@ import { useTaskYjsSync } from './task-edit-dialog/hooks/use-task-yjs-sync';
|
|
|
85
95
|
import { PersonalOverridesSection } from './task-edit-dialog/personal-overrides-section';
|
|
86
96
|
import { TaskActivitySection } from './task-edit-dialog/task-activity-section';
|
|
87
97
|
import { TaskDeleteDialog } from './task-edit-dialog/task-delete-dialog';
|
|
98
|
+
import { TaskDescriptionRestoreBanner } from './task-edit-dialog/task-description-restore-banner';
|
|
99
|
+
import { TaskDescriptionVersionRestoreDialog } from './task-edit-dialog/task-description-version-restore-dialog';
|
|
88
100
|
import { TaskInstancesSection } from './task-edit-dialog/task-instances-section';
|
|
89
101
|
import { TaskPropertiesSection } from './task-edit-dialog/task-properties-section';
|
|
90
102
|
import { TaskRelationshipsProperties } from './task-edit-dialog/task-relationships-properties';
|
|
@@ -104,16 +116,22 @@ import {
|
|
|
104
116
|
} from './task-edit-dialog/user-display';
|
|
105
117
|
import {
|
|
106
118
|
broadcastTaskDescriptionUpsert,
|
|
119
|
+
canPersistTaskDescriptionSnapshot,
|
|
107
120
|
clearDraft,
|
|
121
|
+
createTaskDescriptionPersistenceGuardState,
|
|
108
122
|
getDescriptionContent,
|
|
109
123
|
getDraftStorageKey,
|
|
110
124
|
getTaskDescriptionPercentLeft,
|
|
111
125
|
getTaskDescriptionPreviewText,
|
|
112
126
|
getTaskDescriptionStorageLength,
|
|
127
|
+
normalizeTaskDescriptionSnapshot,
|
|
128
|
+
recordTaskDescriptionEditorSnapshot,
|
|
113
129
|
saveAndVerifyYjsDescriptionToDatabase,
|
|
114
130
|
saveYjsDescriptionToDatabase,
|
|
115
|
-
|
|
131
|
+
serializeTaskDescriptionPersistenceSnapshot,
|
|
116
132
|
shouldPreserveNativeContextMenu,
|
|
133
|
+
type TaskDescriptionPersistenceGuardState,
|
|
134
|
+
updateTaskDescriptionCaches,
|
|
117
135
|
} from './task-edit-dialog/utils';
|
|
118
136
|
import {
|
|
119
137
|
resolveInlineTaskTargetList,
|
|
@@ -125,6 +143,11 @@ import { UnsavedChangesWarningDialog } from './unsaved-changes-warning-dialog';
|
|
|
125
143
|
|
|
126
144
|
type AssigneeMemberSource = 'workspace' | 'board' | 'workspace-and-board';
|
|
127
145
|
|
|
146
|
+
const EMPTY_TASK_DESCRIPTION_DOC: JSONContent = {
|
|
147
|
+
type: 'doc',
|
|
148
|
+
content: [{ type: 'paragraph' }],
|
|
149
|
+
};
|
|
150
|
+
|
|
128
151
|
export {
|
|
129
152
|
type DialogHeaderInfo,
|
|
130
153
|
getTaskDialogHeaderInfo,
|
|
@@ -216,10 +239,12 @@ export function TaskEditDialog({
|
|
|
216
239
|
const isCreateMode = mode === 'create';
|
|
217
240
|
const effectiveTaskWsId = !isCreateMode ? (taskWsId ?? wsId) : wsId;
|
|
218
241
|
const pathname = usePathname();
|
|
242
|
+
const locale = useLocale();
|
|
219
243
|
const queryClient = useQueryClient();
|
|
220
244
|
const t = useTranslations('common');
|
|
221
245
|
const rootT = useTranslations();
|
|
222
246
|
const dialogT = useTranslations('ws-task-boards.dialog');
|
|
247
|
+
const historyT = useTranslations('tasks.history');
|
|
223
248
|
const { registerCloseRequestHandler } = useTaskDialogContext();
|
|
224
249
|
|
|
225
250
|
// Access workspace tier for tier-aware collaboration settings (null outside the provider)
|
|
@@ -290,7 +315,18 @@ export function TaskEditDialog({
|
|
|
290
315
|
const handleConvertToTaskRef = useRef<(() => Promise<void>) | null>(null);
|
|
291
316
|
const descriptionRef = useRef<JSONContent | null>(formState.description);
|
|
292
317
|
const persistedDescriptionRef = useRef<string | null>(null);
|
|
318
|
+
const descriptionPersistenceGuardRef =
|
|
319
|
+
useRef<TaskDescriptionPersistenceGuardState>(
|
|
320
|
+
createTaskDescriptionPersistenceGuardState({
|
|
321
|
+
persistedDescription: null,
|
|
322
|
+
})
|
|
323
|
+
);
|
|
293
324
|
const taskRealtimeBroadcastRef = useRef<BoardBroadcastFn | null>(null);
|
|
325
|
+
const [hasHydratedYjsState, setHasHydratedYjsState] = useState(false);
|
|
326
|
+
const [showDescriptionVersionsDialog, setShowDescriptionVersionsDialog] =
|
|
327
|
+
useState(false);
|
|
328
|
+
const [restoringDescriptionVersionId, setRestoringDescriptionVersionId] =
|
|
329
|
+
useState<string | null>(null);
|
|
294
330
|
|
|
295
331
|
useEffect(() => {
|
|
296
332
|
descriptionRef.current = formState.description;
|
|
@@ -367,6 +403,54 @@ export function TaskEditDialog({
|
|
|
367
403
|
refetchOnReconnect: false,
|
|
368
404
|
});
|
|
369
405
|
|
|
406
|
+
const persistedTaskDescriptionSnapshot = useMemo(
|
|
407
|
+
() =>
|
|
408
|
+
serializeTaskDescriptionPersistenceSnapshot(
|
|
409
|
+
getDescriptionContent(task?.description)
|
|
410
|
+
) ?? null,
|
|
411
|
+
[task?.description]
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
useEffect(() => {
|
|
415
|
+
if (!isOpen || isCreateMode) {
|
|
416
|
+
persistedDescriptionRef.current = null;
|
|
417
|
+
descriptionPersistenceGuardRef.current =
|
|
418
|
+
createTaskDescriptionPersistenceGuardState({
|
|
419
|
+
persistedDescription: null,
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
persistedDescriptionRef.current = persistedTaskDescriptionSnapshot;
|
|
425
|
+
descriptionPersistenceGuardRef.current =
|
|
426
|
+
createTaskDescriptionPersistenceGuardState({
|
|
427
|
+
persistedDescription: persistedTaskDescriptionSnapshot,
|
|
428
|
+
trustPersistedDescription: !effectiveRealtimeEnabled,
|
|
429
|
+
});
|
|
430
|
+
}, [
|
|
431
|
+
effectiveRealtimeEnabled,
|
|
432
|
+
isOpen,
|
|
433
|
+
isCreateMode,
|
|
434
|
+
persistedTaskDescriptionSnapshot,
|
|
435
|
+
]);
|
|
436
|
+
|
|
437
|
+
const canConfirmEmptyDescriptionSnapshot =
|
|
438
|
+
!effectiveRealtimeEnabled || hasHydratedYjsState;
|
|
439
|
+
|
|
440
|
+
const recordDescriptionSnapshot = useCallback(
|
|
441
|
+
(nextDescription: JSONContent | null) => {
|
|
442
|
+
descriptionPersistenceGuardRef.current =
|
|
443
|
+
recordTaskDescriptionEditorSnapshot(
|
|
444
|
+
descriptionPersistenceGuardRef.current,
|
|
445
|
+
nextDescription,
|
|
446
|
+
{
|
|
447
|
+
canConfirmEmptySnapshot: canConfirmEmptyDescriptionSnapshot,
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
},
|
|
451
|
+
[canConfirmEmptyDescriptionSnapshot]
|
|
452
|
+
);
|
|
453
|
+
|
|
370
454
|
const loadTaskDescriptionState = useCallback(async () => {
|
|
371
455
|
if (!task?.id) return null;
|
|
372
456
|
const response = await fetchWorkspaceTaskDescription(
|
|
@@ -393,7 +477,9 @@ export function TaskEditDialog({
|
|
|
393
477
|
return false;
|
|
394
478
|
}
|
|
395
479
|
|
|
396
|
-
|
|
480
|
+
content = normalizeTaskDescriptionSnapshot(content);
|
|
481
|
+
const serializedDescription =
|
|
482
|
+
serializeTaskDescriptionPersistenceSnapshot(content);
|
|
397
483
|
|
|
398
484
|
if (
|
|
399
485
|
serializedDescription &&
|
|
@@ -402,6 +488,15 @@ export function TaskEditDialog({
|
|
|
402
488
|
return false;
|
|
403
489
|
}
|
|
404
490
|
|
|
491
|
+
if (
|
|
492
|
+
!canPersistTaskDescriptionSnapshot({
|
|
493
|
+
currentSerializedDescription: serializedDescription,
|
|
494
|
+
guardState: descriptionPersistenceGuardRef.current,
|
|
495
|
+
})
|
|
496
|
+
) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
|
|
405
500
|
const didPersist = await saveYjsDescriptionToDatabase({
|
|
406
501
|
wsId: effectiveTaskWsId,
|
|
407
502
|
taskId: task.id,
|
|
@@ -415,6 +510,11 @@ export function TaskEditDialog({
|
|
|
415
510
|
if (!didPersist) return false;
|
|
416
511
|
|
|
417
512
|
persistedDescriptionRef.current = serializedDescription;
|
|
513
|
+
descriptionPersistenceGuardRef.current =
|
|
514
|
+
createTaskDescriptionPersistenceGuardState({
|
|
515
|
+
persistedDescription: serializedDescription,
|
|
516
|
+
trustPersistedDescription: true,
|
|
517
|
+
});
|
|
418
518
|
|
|
419
519
|
const broadcast =
|
|
420
520
|
getActiveBroadcast() ?? taskRealtimeBroadcastRef.current;
|
|
@@ -444,8 +544,6 @@ export function TaskEditDialog({
|
|
|
444
544
|
saveDocumentState: saveTaskDescriptionState,
|
|
445
545
|
});
|
|
446
546
|
|
|
447
|
-
const [hasHydratedYjsState, setHasHydratedYjsState] = useState(false);
|
|
448
|
-
|
|
449
547
|
useEffect(() => {
|
|
450
548
|
if (!isOpen || isCreateMode || !effectiveRealtimeEnabled || !task?.id) {
|
|
451
549
|
setHasHydratedYjsState(false);
|
|
@@ -475,6 +573,49 @@ export function TaskEditDialog({
|
|
|
475
573
|
synced,
|
|
476
574
|
]);
|
|
477
575
|
|
|
576
|
+
const { data: descriptionHistoryData } = useQuery({
|
|
577
|
+
queryKey: ['task-history', effectiveTaskWsId, task?.id, 'description'],
|
|
578
|
+
queryFn: async () => {
|
|
579
|
+
if (!task?.id) return null;
|
|
580
|
+
|
|
581
|
+
return getWorkspaceTaskHistory(effectiveTaskWsId, task.id, {
|
|
582
|
+
limit: 100,
|
|
583
|
+
changeType: 'field_updated',
|
|
584
|
+
fieldName: 'description',
|
|
585
|
+
});
|
|
586
|
+
},
|
|
587
|
+
enabled:
|
|
588
|
+
isOpen &&
|
|
589
|
+
!isCreateMode &&
|
|
590
|
+
!taskControlsDisabled &&
|
|
591
|
+
Boolean(effectiveTaskWsId) &&
|
|
592
|
+
Boolean(task?.id),
|
|
593
|
+
staleTime: 30 * 1000,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const recoverableDescriptionVersions = useMemo(
|
|
597
|
+
() =>
|
|
598
|
+
buildRecoverableTaskDescriptionVersions(
|
|
599
|
+
descriptionHistoryData?.history ?? []
|
|
600
|
+
),
|
|
601
|
+
[descriptionHistoryData?.history]
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
const currentSerializedDescription = useMemo(
|
|
605
|
+
() =>
|
|
606
|
+
serializeTaskDescriptionPersistenceSnapshot(formState.description) ??
|
|
607
|
+
null,
|
|
608
|
+
[formState.description]
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
const latestRestorableDescriptionVersion = useMemo(() => {
|
|
612
|
+
const latestVersion = recoverableDescriptionVersions[0] ?? null;
|
|
613
|
+
if (!latestVersion) return null;
|
|
614
|
+
return latestVersion.description === currentSerializedDescription
|
|
615
|
+
? null
|
|
616
|
+
: latestVersion;
|
|
617
|
+
}, [currentSerializedDescription, recoverableDescriptionVersions]);
|
|
618
|
+
|
|
478
619
|
// Update user when props change
|
|
479
620
|
useEffect(() => {
|
|
480
621
|
if (propsCurrentUser) {
|
|
@@ -784,34 +925,45 @@ export function TaskEditDialog({
|
|
|
784
925
|
});
|
|
785
926
|
|
|
786
927
|
// Change detection
|
|
787
|
-
const { hasUnsavedChanges, canSave
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
});
|
|
928
|
+
const { hasUnsavedChanges, canSave } = useTaskChangeDetection({
|
|
929
|
+
task,
|
|
930
|
+
name: formState.name,
|
|
931
|
+
description: formState.description,
|
|
932
|
+
priority: formState.priority,
|
|
933
|
+
startDate: formState.startDate,
|
|
934
|
+
endDate: formState.endDate,
|
|
935
|
+
selectedListId: formState.selectedListId,
|
|
936
|
+
estimationPoints: formState.estimationPoints,
|
|
937
|
+
selectedLabels: formState.selectedLabels,
|
|
938
|
+
selectedAssignees: formState.selectedAssignees,
|
|
939
|
+
isCreateMode,
|
|
940
|
+
isLoading: isLoading || taskControlsDisabled,
|
|
941
|
+
collaborationMode: effectiveCollaborationMode,
|
|
942
|
+
draftId,
|
|
943
|
+
});
|
|
804
944
|
|
|
805
945
|
useEffect(() => {
|
|
806
|
-
if (!isOpen || isCreateMode)
|
|
807
|
-
|
|
946
|
+
if (!isOpen || isCreateMode) return;
|
|
947
|
+
if (effectiveRealtimeEnabled && !hasHydratedYjsState) return;
|
|
948
|
+
if (
|
|
949
|
+
serializeTaskDescriptionPersistenceSnapshot(formState.description) ===
|
|
950
|
+
null
|
|
951
|
+
) {
|
|
808
952
|
return;
|
|
809
953
|
}
|
|
810
954
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
955
|
+
descriptionPersistenceGuardRef.current =
|
|
956
|
+
recordTaskDescriptionEditorSnapshot(
|
|
957
|
+
descriptionPersistenceGuardRef.current,
|
|
958
|
+
formState.description
|
|
959
|
+
);
|
|
960
|
+
}, [
|
|
961
|
+
effectiveRealtimeEnabled,
|
|
962
|
+
formState.description,
|
|
963
|
+
hasHydratedYjsState,
|
|
964
|
+
isOpen,
|
|
965
|
+
isCreateMode,
|
|
966
|
+
]);
|
|
815
967
|
|
|
816
968
|
// Realtime sync for task fields and relations. Description sync remains Yjs-only.
|
|
817
969
|
const { broadcast: taskRealtimeBroadcast } = useTaskRealtimeSync({
|
|
@@ -1028,6 +1180,135 @@ export function TaskEditDialog({
|
|
|
1028
1180
|
);
|
|
1029
1181
|
}, []);
|
|
1030
1182
|
|
|
1183
|
+
const applyRestoredDescriptionToOpenEditor = useCallback(
|
|
1184
|
+
({
|
|
1185
|
+
content,
|
|
1186
|
+
yjsState,
|
|
1187
|
+
}: {
|
|
1188
|
+
content: JSONContent | null;
|
|
1189
|
+
yjsState: number[] | null;
|
|
1190
|
+
}) => {
|
|
1191
|
+
const nextContent = content ?? EMPTY_TASK_DESCRIPTION_DOC;
|
|
1192
|
+
|
|
1193
|
+
const nextYjsState =
|
|
1194
|
+
yjsState && yjsState.length > 0
|
|
1195
|
+
? Uint8Array.from(yjsState)
|
|
1196
|
+
: doc && editorInstance?.schema
|
|
1197
|
+
? convertJsonContentToYjsState(nextContent, editorInstance.schema)
|
|
1198
|
+
: null;
|
|
1199
|
+
|
|
1200
|
+
if (doc && nextYjsState) {
|
|
1201
|
+
const syncOrigin = provider ?? SUPABASE_PROVIDER_SYNC_ORIGIN;
|
|
1202
|
+
doc.transact(() => {
|
|
1203
|
+
const fragment = doc.getXmlFragment('prosemirror');
|
|
1204
|
+
if (fragment.length > 0) {
|
|
1205
|
+
fragment.delete(0, fragment.length);
|
|
1206
|
+
}
|
|
1207
|
+
}, syncOrigin);
|
|
1208
|
+
Y.applyUpdate(doc, nextYjsState, syncOrigin);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
editorInstance?.commands.setContent(nextContent, {
|
|
1213
|
+
emitUpdate: false,
|
|
1214
|
+
});
|
|
1215
|
+
},
|
|
1216
|
+
[doc, editorInstance, provider]
|
|
1217
|
+
);
|
|
1218
|
+
|
|
1219
|
+
const handleRestoreDescriptionVersion = useCallback(
|
|
1220
|
+
async (version: RecoverableTaskDescriptionVersion) => {
|
|
1221
|
+
if (!task?.id) return;
|
|
1222
|
+
|
|
1223
|
+
setRestoringDescriptionVersionId(version.id);
|
|
1224
|
+
|
|
1225
|
+
try {
|
|
1226
|
+
const response = await updateWorkspaceTaskDescription(
|
|
1227
|
+
effectiveTaskWsId,
|
|
1228
|
+
task.id,
|
|
1229
|
+
{ description: version.description }
|
|
1230
|
+
);
|
|
1231
|
+
const restoredDescription = response.description ?? version.description;
|
|
1232
|
+
const restoredContent = normalizeTaskDescriptionSnapshot(
|
|
1233
|
+
getDescriptionContent(restoredDescription)
|
|
1234
|
+
);
|
|
1235
|
+
const serializedRestoredDescription =
|
|
1236
|
+
serializeTaskDescriptionPersistenceSnapshot(restoredContent) ?? null;
|
|
1237
|
+
|
|
1238
|
+
applyRestoredDescriptionToOpenEditor({
|
|
1239
|
+
content: restoredContent,
|
|
1240
|
+
yjsState: response.description_yjs_state,
|
|
1241
|
+
});
|
|
1242
|
+
formState.setDescription(restoredContent);
|
|
1243
|
+
setDescriptionStorageLength(
|
|
1244
|
+
getTaskDescriptionStorageLength(restoredContent)
|
|
1245
|
+
);
|
|
1246
|
+
persistedDescriptionRef.current = serializedRestoredDescription;
|
|
1247
|
+
descriptionPersistenceGuardRef.current =
|
|
1248
|
+
createTaskDescriptionPersistenceGuardState({
|
|
1249
|
+
persistedDescription: serializedRestoredDescription,
|
|
1250
|
+
trustPersistedDescription: true,
|
|
1251
|
+
});
|
|
1252
|
+
setHasHydratedYjsState(true);
|
|
1253
|
+
|
|
1254
|
+
updateTaskDescriptionCaches({
|
|
1255
|
+
taskId: task.id,
|
|
1256
|
+
descriptionString: serializedRestoredDescription,
|
|
1257
|
+
boardId,
|
|
1258
|
+
queryClient,
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
await Promise.all([
|
|
1262
|
+
queryClient.invalidateQueries({
|
|
1263
|
+
queryKey: ['task-history', effectiveTaskWsId, task.id],
|
|
1264
|
+
}),
|
|
1265
|
+
queryClient.invalidateQueries({
|
|
1266
|
+
queryKey: ['task', task.id],
|
|
1267
|
+
}),
|
|
1268
|
+
queryClient.invalidateQueries({
|
|
1269
|
+
queryKey: ['task-snapshot', effectiveTaskWsId, task.id],
|
|
1270
|
+
}),
|
|
1271
|
+
]);
|
|
1272
|
+
|
|
1273
|
+
const broadcast =
|
|
1274
|
+
getActiveBroadcast() ?? taskRealtimeBroadcastRef.current;
|
|
1275
|
+
broadcastTaskDescriptionUpsert({
|
|
1276
|
+
taskId: task.id,
|
|
1277
|
+
descriptionString: serializedRestoredDescription,
|
|
1278
|
+
broadcast: broadcast ?? undefined,
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
onUpdate();
|
|
1282
|
+
setShowDescriptionVersionsDialog(false);
|
|
1283
|
+
toast.success(
|
|
1284
|
+
historyT('description_restore_success', {
|
|
1285
|
+
defaultValue: 'Description restored',
|
|
1286
|
+
})
|
|
1287
|
+
);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
toast.error(
|
|
1290
|
+
error instanceof Error
|
|
1291
|
+
? error.message
|
|
1292
|
+
: historyT('description_restore_failed', {
|
|
1293
|
+
defaultValue: 'Failed to restore description',
|
|
1294
|
+
})
|
|
1295
|
+
);
|
|
1296
|
+
} finally {
|
|
1297
|
+
setRestoringDescriptionVersionId(null);
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
[
|
|
1301
|
+
applyRestoredDescriptionToOpenEditor,
|
|
1302
|
+
boardId,
|
|
1303
|
+
effectiveTaskWsId,
|
|
1304
|
+
formState.setDescription,
|
|
1305
|
+
historyT,
|
|
1306
|
+
onUpdate,
|
|
1307
|
+
queryClient,
|
|
1308
|
+
task?.id,
|
|
1309
|
+
]
|
|
1310
|
+
);
|
|
1311
|
+
|
|
1031
1312
|
const isDescriptionOverLimit =
|
|
1032
1313
|
descriptionStorageLength > MAX_TASK_DESCRIPTION_LENGTH;
|
|
1033
1314
|
const descriptionPercentLeft = getTaskDescriptionPercentLeft(
|
|
@@ -1393,7 +1674,8 @@ export function TaskEditDialog({
|
|
|
1393
1674
|
return createWorkspaceTaskSuggestions(effectiveTaskWsId, {
|
|
1394
1675
|
boardId,
|
|
1395
1676
|
prompt,
|
|
1396
|
-
description:
|
|
1677
|
+
description:
|
|
1678
|
+
serializeTaskDescriptionPersistenceSnapshot(currentDescription),
|
|
1397
1679
|
currentListId: formState.selectedListId || undefined,
|
|
1398
1680
|
clientTimezone:
|
|
1399
1681
|
Intl.DateTimeFormat().resolvedOptions().timeZone || undefined,
|
|
@@ -1690,9 +1972,11 @@ export function TaskEditDialog({
|
|
|
1690
1972
|
|
|
1691
1973
|
closeBlockedByOverflowRef.current = false;
|
|
1692
1974
|
|
|
1693
|
-
const currentContent =
|
|
1975
|
+
const currentContent = normalizeTaskDescriptionSnapshot(
|
|
1976
|
+
flushEditorPendingRef.current()
|
|
1977
|
+
);
|
|
1694
1978
|
const currentSerializedDescription =
|
|
1695
|
-
|
|
1979
|
+
serializeTaskDescriptionPersistenceSnapshot(currentContent) ?? null;
|
|
1696
1980
|
|
|
1697
1981
|
if (
|
|
1698
1982
|
currentSerializedDescription &&
|
|
@@ -1709,6 +1993,15 @@ export function TaskEditDialog({
|
|
|
1709
1993
|
return true;
|
|
1710
1994
|
}
|
|
1711
1995
|
|
|
1996
|
+
if (
|
|
1997
|
+
!canPersistTaskDescriptionSnapshot({
|
|
1998
|
+
currentSerializedDescription,
|
|
1999
|
+
guardState: descriptionPersistenceGuardRef.current,
|
|
2000
|
+
})
|
|
2001
|
+
) {
|
|
2002
|
+
return false;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1712
2005
|
const yjsState =
|
|
1713
2006
|
currentContent && editorInstance?.schema
|
|
1714
2007
|
? Array.from(
|
|
@@ -1728,6 +2021,11 @@ export function TaskEditDialog({
|
|
|
1728
2021
|
|
|
1729
2022
|
if (didPersist) {
|
|
1730
2023
|
persistedDescriptionRef.current = currentSerializedDescription;
|
|
2024
|
+
descriptionPersistenceGuardRef.current =
|
|
2025
|
+
createTaskDescriptionPersistenceGuardState({
|
|
2026
|
+
persistedDescription: currentSerializedDescription,
|
|
2027
|
+
trustPersistedDescription: true,
|
|
2028
|
+
});
|
|
1731
2029
|
}
|
|
1732
2030
|
|
|
1733
2031
|
return didPersist;
|
|
@@ -1745,9 +2043,11 @@ export function TaskEditDialog({
|
|
|
1745
2043
|
return false;
|
|
1746
2044
|
}
|
|
1747
2045
|
|
|
1748
|
-
const currentContent =
|
|
2046
|
+
const currentContent = normalizeTaskDescriptionSnapshot(
|
|
2047
|
+
flushEditorPendingRef.current()
|
|
2048
|
+
);
|
|
1749
2049
|
const currentSerializedDescription =
|
|
1750
|
-
|
|
2050
|
+
serializeTaskDescriptionPersistenceSnapshot(currentContent) ?? null;
|
|
1751
2051
|
|
|
1752
2052
|
if (
|
|
1753
2053
|
currentSerializedDescription &&
|
|
@@ -2521,6 +2821,30 @@ export function TaskEditDialog({
|
|
|
2521
2821
|
/>
|
|
2522
2822
|
)}
|
|
2523
2823
|
|
|
2824
|
+
{!taskControlsDisabled && !isCreateMode && (
|
|
2825
|
+
<TaskDescriptionRestoreBanner
|
|
2826
|
+
isRestoring={Boolean(restoringDescriptionVersionId)}
|
|
2827
|
+
latestVersion={latestRestorableDescriptionVersion}
|
|
2828
|
+
onRestoreLatest={() => {
|
|
2829
|
+
if (latestRestorableDescriptionVersion) {
|
|
2830
|
+
void handleRestoreDescriptionVersion(
|
|
2831
|
+
latestRestorableDescriptionVersion
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
}}
|
|
2835
|
+
onViewVersions={() =>
|
|
2836
|
+
setShowDescriptionVersionsDialog(true)
|
|
2837
|
+
}
|
|
2838
|
+
t={
|
|
2839
|
+
historyT as (
|
|
2840
|
+
key: string,
|
|
2841
|
+
options?: { count?: number; defaultValue?: string }
|
|
2842
|
+
) => string
|
|
2843
|
+
}
|
|
2844
|
+
versionCount={recoverableDescriptionVersions.length}
|
|
2845
|
+
/>
|
|
2846
|
+
)}
|
|
2847
|
+
|
|
2524
2848
|
<TaskDescriptionEditor
|
|
2525
2849
|
description={formState.description}
|
|
2526
2850
|
setDescription={formState.setDescription}
|
|
@@ -2554,6 +2878,7 @@ export function TaskEditDialog({
|
|
|
2554
2878
|
onImageUpload={imageUploadHandler}
|
|
2555
2879
|
onEditorReady={handleEditorReady}
|
|
2556
2880
|
onConvertToTask={handleConvertToTask}
|
|
2881
|
+
onDescriptionSnapshotChange={recordDescriptionSnapshot}
|
|
2557
2882
|
onDescriptionStorageLengthChange={
|
|
2558
2883
|
handleDescriptionStorageLengthChange
|
|
2559
2884
|
}
|
|
@@ -2633,7 +2958,13 @@ export function TaskEditDialog({
|
|
|
2633
2958
|
id: p.id,
|
|
2634
2959
|
})),
|
|
2635
2960
|
}}
|
|
2961
|
+
onRestoreDescriptionVersion={
|
|
2962
|
+
handleRestoreDescriptionVersion
|
|
2963
|
+
}
|
|
2636
2964
|
revertDisabled={true}
|
|
2965
|
+
restoringDescriptionVersionId={
|
|
2966
|
+
restoringDescriptionVersionId
|
|
2967
|
+
}
|
|
2637
2968
|
/>
|
|
2638
2969
|
)}
|
|
2639
2970
|
</div>
|
|
@@ -2655,6 +2986,22 @@ export function TaskEditDialog({
|
|
|
2655
2986
|
</DialogContent>
|
|
2656
2987
|
</Dialog>
|
|
2657
2988
|
|
|
2989
|
+
<TaskDescriptionVersionRestoreDialog
|
|
2990
|
+
currentDescription={currentSerializedDescription}
|
|
2991
|
+
isOpen={showDescriptionVersionsDialog}
|
|
2992
|
+
locale={locale}
|
|
2993
|
+
onClose={() => setShowDescriptionVersionsDialog(false)}
|
|
2994
|
+
onRestoreVersion={handleRestoreDescriptionVersion}
|
|
2995
|
+
restoringVersionId={restoringDescriptionVersionId}
|
|
2996
|
+
t={
|
|
2997
|
+
historyT as (
|
|
2998
|
+
key: string,
|
|
2999
|
+
options?: { count?: number; defaultValue?: string }
|
|
3000
|
+
) => string
|
|
3001
|
+
}
|
|
3002
|
+
versions={recoverableDescriptionVersions}
|
|
3003
|
+
/>
|
|
3004
|
+
|
|
2658
3005
|
<TaskDeleteDialog
|
|
2659
3006
|
open={showDeleteConfirm}
|
|
2660
3007
|
onOpenChange={setShowDeleteConfirm}
|