@parhelia/core 0.3.12788 → 0.3.12791
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/dist/components/ui/LanguageSelector.js +4 -1
- package/dist/components/ui/LanguageSelector.js.map +1 -1
- package/dist/components/ui/select.js +12 -1
- package/dist/components/ui/select.js.map +1 -1
- package/dist/editor/FieldHistory.js +2 -2
- package/dist/editor/FieldHistory.js.map +1 -1
- package/dist/editor/ai/InlineAiDialog.js +35 -8
- package/dist/editor/ai/InlineAiDialog.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentProjectProgress.d.ts +4 -3
- package/dist/editor/ai/terminal/components/AgentProjectProgress.js +34 -50
- package/dist/editor/ai/terminal/components/AgentProjectProgress.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentTerminalView.d.ts +2 -1
- package/dist/editor/ai/terminal/components/AgentTerminalView.js +2 -2
- package/dist/editor/ai/terminal/components/AgentTerminalView.js.map +1 -1
- package/dist/editor/ai/terminal/useAgentTerminalController.js +1 -0
- package/dist/editor/ai/terminal/useAgentTerminalController.js.map +1 -1
- package/dist/editor/ai/terminal/useAgentTerminalViewProps.d.ts +1 -0
- package/dist/editor/ai/terminal/useAgentTerminalViewProps.js +2 -1
- package/dist/editor/ai/terminal/useAgentTerminalViewProps.js.map +1 -1
- package/dist/editor/bridge/protocol.d.ts +4 -0
- package/dist/editor/bridge/protocol.js +1 -0
- package/dist/editor/bridge/protocol.js.map +1 -1
- package/dist/editor/client/EditorShell.js +174 -65
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +13 -3
- package/dist/editor/client/editContext.js.map +1 -1
- package/dist/editor/client/hooks/activeSessionSnapshots.d.ts +2 -0
- package/dist/editor/client/hooks/activeSessionSnapshots.js +43 -0
- package/dist/editor/client/hooks/activeSessionSnapshots.js.map +1 -0
- package/dist/editor/client/hooks/feedbackSocketState.js +3 -0
- package/dist/editor/client/hooks/feedbackSocketState.js.map +1 -1
- package/dist/editor/client/hooks/operationRefreshDeduplication.d.ts +22 -0
- package/dist/editor/client/hooks/operationRefreshDeduplication.js +45 -0
- package/dist/editor/client/hooks/operationRefreshDeduplication.js.map +1 -0
- package/dist/editor/client/hooks/socketMessageRetention.d.ts +7 -0
- package/dist/editor/client/hooks/socketMessageRetention.js +11 -0
- package/dist/editor/client/hooks/socketMessageRetention.js.map +1 -0
- package/dist/editor/client/hooks/useSocketMessageHandler.js +39 -19
- package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
- package/dist/editor/client/hooks/useWorkbox.d.ts +4 -0
- package/dist/editor/client/hooks/useWorkbox.js +132 -2
- package/dist/editor/client/hooks/useWorkbox.js.map +1 -1
- package/dist/editor/client/itemsRepository.d.ts +7 -1
- package/dist/editor/client/itemsRepository.js +41 -18
- package/dist/editor/client/itemsRepository.js.map +1 -1
- package/dist/editor/client/operations.d.ts +7 -3
- package/dist/editor/client/operations.js +216 -150
- package/dist/editor/client/operations.js.map +1 -1
- package/dist/editor/client/pageModelBuilder.js +15 -3
- package/dist/editor/client/pageModelBuilder.js.map +1 -1
- package/dist/editor/client/sidebarAvailability.d.ts +4 -0
- package/dist/editor/client/sidebarAvailability.js +15 -0
- package/dist/editor/client/sidebarAvailability.js.map +1 -1
- package/dist/editor/commands/itemCommands.js +1 -1
- package/dist/editor/commands/itemCommands.js.map +1 -1
- package/dist/editor/field-types/RichTextEditorComponent.js +6 -1
- package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
- package/dist/editor/field-types/SingleLineText.js +2 -3
- package/dist/editor/field-types/SingleLineText.js.map +1 -1
- package/dist/editor/field-types/richtext/components/ReactSlate.d.ts +6 -1
- package/dist/editor/field-types/richtext/components/ReactSlate.js +91 -5
- package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
- package/dist/editor/field-types/singleLineTextValue.d.ts +3 -0
- package/dist/editor/field-types/singleLineTextValue.js +9 -0
- package/dist/editor/field-types/singleLineTextValue.js.map +1 -0
- package/dist/editor/menubar/ItemLanguageVersion.js +1 -1
- package/dist/editor/menubar/ItemLanguageVersion.js.map +1 -1
- package/dist/editor/menubar/VersionSelector.js +18 -6
- package/dist/editor/menubar/VersionSelector.js.map +1 -1
- package/dist/editor/menubar/WorkflowButton.js +8 -3
- package/dist/editor/menubar/WorkflowButton.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/EditControls.js +1 -1
- package/dist/editor/menubar/toolbar-sections/EditControls.js.map +1 -1
- package/dist/editor/notifications/NotificationCenter.js +26 -7
- package/dist/editor/notifications/NotificationCenter.js.map +1 -1
- package/dist/editor/notifications/useNotifications.d.ts +3 -1
- package/dist/editor/notifications/useNotifications.js +27 -9
- package/dist/editor/notifications/useNotifications.js.map +1 -1
- package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.js +18 -0
- package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.js.map +1 -1
- package/dist/editor/page-editor-chrome/InlineFieldHistoryButton.js +30 -8
- package/dist/editor/page-editor-chrome/InlineFieldHistoryButton.js.map +1 -1
- package/dist/editor/page-editor-chrome/VersionDiffHighlightings.js +30 -8
- package/dist/editor/page-editor-chrome/VersionDiffHighlightings.js.map +1 -1
- package/dist/editor/page-editor-chrome/useBridgeInlineEditing.d.ts +5 -3
- package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js +111 -19
- package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js.map +1 -1
- package/dist/editor/page-viewer/EditorForm.d.ts +2 -1
- package/dist/editor/page-viewer/EditorForm.js +41 -12
- package/dist/editor/page-viewer/EditorForm.js.map +1 -1
- package/dist/editor/page-viewer/PageViewer.d.ts +2 -0
- package/dist/editor/page-viewer/PageViewer.js +11 -2
- package/dist/editor/page-viewer/PageViewer.js.map +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.js +237 -42
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/page-viewer/bridgeInlineAiSelection.d.ts +17 -0
- package/dist/editor/page-viewer/bridgeInlineAiSelection.js +80 -0
- package/dist/editor/page-viewer/bridgeInlineAiSelection.js.map +1 -0
- package/dist/editor/page-viewer/bridgeInlineEditCommit.d.ts +18 -0
- package/dist/editor/page-viewer/bridgeInlineEditCommit.js +24 -0
- package/dist/editor/page-viewer/bridgeInlineEditCommit.js.map +1 -0
- package/dist/editor/page-viewer/bridgeInlineEditSession.d.ts +3 -0
- package/dist/editor/page-viewer/bridgeInlineEditSession.js +30 -0
- package/dist/editor/page-viewer/bridgeInlineEditSession.js.map +1 -1
- package/dist/editor/page-viewer/bridgeItemChangeTarget.d.ts +9 -0
- package/dist/editor/page-viewer/bridgeItemChangeTarget.js +38 -0
- package/dist/editor/page-viewer/bridgeItemChangeTarget.js.map +1 -0
- package/dist/editor/page-viewer/bridgePointerDragRelease.d.ts +31 -0
- package/dist/editor/page-viewer/bridgePointerDragRelease.js +32 -0
- package/dist/editor/page-viewer/bridgePointerDragRelease.js.map +1 -0
- package/dist/editor/page-viewer/renderUrl.d.ts +8 -0
- package/dist/editor/page-viewer/renderUrl.js +10 -0
- package/dist/editor/page-viewer/renderUrl.js.map +1 -1
- package/dist/editor/personalization/PersonalizationPanel.js +52 -25
- package/dist/editor/personalization/PersonalizationPanel.js.map +1 -1
- package/dist/editor/reviews/Comment.js +3 -1
- package/dist/editor/reviews/Comment.js.map +1 -1
- package/dist/editor/reviews/CommentDisplayPopover.js +2 -0
- package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
- package/dist/editor/reviews/Comments.js +16 -3
- package/dist/editor/reviews/Comments.js.map +1 -1
- package/dist/editor/reviews/SuggestedEdit.js +25 -2
- package/dist/editor/reviews/SuggestedEdit.js.map +1 -1
- package/dist/editor/reviews/SuggestionCommentThread.js +2 -0
- package/dist/editor/reviews/SuggestionCommentThread.js.map +1 -1
- package/dist/editor/reviews/applySuggestedEditActions.js +105 -25
- package/dist/editor/reviews/applySuggestedEditActions.js.map +1 -1
- package/dist/editor/reviews/commentState.d.ts +4 -0
- package/dist/editor/reviews/commentState.js +15 -0
- package/dist/editor/reviews/commentState.js.map +1 -0
- package/dist/editor/reviews/feedbackOrdering.d.ts +1 -0
- package/dist/editor/reviews/feedbackOrdering.js +6 -0
- package/dist/editor/reviews/feedbackOrdering.js.map +1 -1
- package/dist/editor/reviews/suggestionContentChange.d.ts +7 -0
- package/dist/editor/reviews/suggestionContentChange.js +10 -0
- package/dist/editor/reviews/suggestionContentChange.js.map +1 -1
- package/dist/editor/reviews/suggestionPatch.js +6 -0
- package/dist/editor/reviews/suggestionPatch.js.map +1 -1
- package/dist/editor/services/personalizationService.js +30 -6
- package/dist/editor/services/personalizationService.js.map +1 -1
- package/dist/editor/services/suggestedEditsService.d.ts +1 -0
- package/dist/editor/services/suggestedEditsService.js +3 -0
- package/dist/editor/services/suggestedEditsService.js.map +1 -1
- package/dist/editor/settings/SettingsView.js +15 -7
- package/dist/editor/settings/SettingsView.js.map +1 -1
- package/dist/editor/settings/panels/MemoriesPanel.js +3 -2
- package/dist/editor/settings/panels/MemoriesPanel.js.map +1 -1
- package/dist/editor/settings/panels/ProjectTemplatesPanel.d.ts +3 -0
- package/dist/editor/settings/panels/ProjectTemplatesPanel.js +84 -15
- package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -1
- package/dist/editor/settings/panels/projectTemplateDuplication.d.ts +5 -0
- package/dist/editor/settings/panels/projectTemplateDuplication.js +52 -0
- package/dist/editor/settings/panels/projectTemplateDuplication.js.map +1 -0
- package/dist/editor/ui/PerfectTree.js +2 -2
- package/dist/editor/ui/PerfectTree.js.map +1 -1
- package/dist/editor/ui/PublishRestrictionsDialog.js +9 -1
- package/dist/editor/ui/PublishRestrictionsDialog.js.map +1 -1
- package/dist/editor/utils.js +5 -2
- package/dist/editor/utils.js.map +1 -1
- package/dist/editor/views/EditorSlot.js +5 -1
- package/dist/editor/views/EditorSlot.js.map +1 -1
- package/dist/editor/views/SingleEditView.d.ts +4 -1
- package/dist/editor/views/SingleEditView.js +2 -2
- package/dist/editor/views/SingleEditView.js.map +1 -1
- package/dist/licensing/LicenseContext.js +7 -0
- package/dist/licensing/LicenseContext.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/task-board/TaskBoardWorkspace.js +5 -1
- package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
- package/dist/task-board/taskExecutionRecords.js +7 -0
- package/dist/task-board/taskExecutionRecords.js.map +1 -1
- package/dist/task-board/views/DependencyGraphView.d.ts +1 -0
- package/dist/task-board/views/DependencyGraphView.js +15 -12
- package/dist/task-board/views/DependencyGraphView.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import { handleErrorResult } from "./helpers";
|
|
|
7
7
|
import { isMissingEditOperationResult } from "./editHistoryReconciliation";
|
|
8
8
|
import { useDebouncedCallback } from "use-debounce";
|
|
9
9
|
import { createOrUpdateSuggestedEdit, deleteSuggestedEdit, } from "../services/suggestedEditsService";
|
|
10
|
-
import { hasSuggestionContentChange, shouldRemoveNoOpSuggestion, } from "../reviews/suggestionContentChange";
|
|
10
|
+
import { hasLiveFieldDivergedFromSuggestion, hasSuggestionContentChange, shouldRemoveNoOpSuggestion, } from "../reviews/suggestionContentChange";
|
|
11
11
|
import { GUID_REGEX_EXACT, getItemDescriptor } from "../utils";
|
|
12
12
|
import { cleanId } from "../utils/id-helper";
|
|
13
13
|
import { copyItemsNeedsTerminalCompletion, normalizeOpId, waitForEditOperationTerminal, } from "./waitForEditOperationTerminal";
|
|
@@ -18,6 +18,7 @@ import { preferLatestLocalUndoTarget } from "./undoTargetResolution";
|
|
|
18
18
|
import { logFieldValueDiagnostic, summarizeFieldDescriptor, summarizeFieldValue, } from "../fieldValueDiagnostics";
|
|
19
19
|
import { getSuggestedEditItemLanguage, getSuggestedEditItemVersion, getSuggestionFieldIdentityKey, } from "../reviews/suggestionDisplayValue";
|
|
20
20
|
import { enqueueSuggestedEditFieldTask, waitForSuggestedEditFieldTasks, } from "./suggestedEditFieldQueue";
|
|
21
|
+
import { markNextBridgeRefreshAuthoritative } from "../page-viewer/bridgeInlineEditCommit";
|
|
21
22
|
/** When true, the delete dialog shows "permanently delete (bypass recycle bin)". */
|
|
22
23
|
const ENABLE_HARD_DELETE_IN_DELETE_DIALOG = false;
|
|
23
24
|
// Track pending suggested edits to prevent creating duplicates before server response
|
|
@@ -378,7 +379,7 @@ export function getOperationsContext(state, ui) {
|
|
|
378
379
|
}
|
|
379
380
|
return true;
|
|
380
381
|
};
|
|
381
|
-
const ensureLock = useCallback(async (field) => {
|
|
382
|
+
const ensureLock = useCallback(async (field, options) => {
|
|
382
383
|
if (!field || !field.item) {
|
|
383
384
|
return false;
|
|
384
385
|
}
|
|
@@ -386,7 +387,7 @@ export function getOperationsContext(state, ui) {
|
|
|
386
387
|
const locallyAcquiredLock = acquiredFieldLockRef.current;
|
|
387
388
|
const isAlreadyLocked = fieldsMatchForLock(stateRef.current.lockedField, field) ||
|
|
388
389
|
fieldsMatchForLock(locallyAcquiredLock, field);
|
|
389
|
-
if (isAlreadyLocked) {
|
|
390
|
+
if (!options?.force && isAlreadyLocked) {
|
|
390
391
|
return true;
|
|
391
392
|
}
|
|
392
393
|
const existingPendingLock = pendingFieldLocks.current.get(lockKey);
|
|
@@ -514,6 +515,11 @@ export function getOperationsContext(state, ui) {
|
|
|
514
515
|
const lastEditField = useRef(undefined);
|
|
515
516
|
const pendingFieldSaves = useRef(new Map());
|
|
516
517
|
const fieldEditGeneration = useRef(0);
|
|
518
|
+
// A blur is an undo-history boundary even when the blurred field's debounced
|
|
519
|
+
// save is still resolving. Capture this generation when an edit starts so a
|
|
520
|
+
// newer focus/edit cannot reuse the operation created by that older save.
|
|
521
|
+
const fieldOperationBoundary = useRef(0);
|
|
522
|
+
const lastOpBoundary = useRef(undefined);
|
|
517
523
|
const completedFieldSaveGeneration = useRef(0);
|
|
518
524
|
const latestSavedFieldEditOperationId = useRef(undefined);
|
|
519
525
|
const getFieldSaveKey = (field) => {
|
|
@@ -548,9 +554,82 @@ export function getOperationsContext(state, ui) {
|
|
|
548
554
|
? stateRef.current.getFieldModificationStoreForSlot?.(activeSlotId)
|
|
549
555
|
: undefined;
|
|
550
556
|
}, []);
|
|
551
|
-
const
|
|
557
|
+
const queueSuggestedEditSave = useCallback((fieldKey, pendingFieldKey, op) => {
|
|
558
|
+
const saveSnapshot = { ...op };
|
|
559
|
+
const expectedNewValue = saveSnapshot.newValue;
|
|
560
|
+
return enqueueSuggestedEditFieldTask(fieldKey, () => {
|
|
561
|
+
const persistedVersion = persistedSuggestedEditVersions.get(fieldKey);
|
|
562
|
+
const snapshotForSave = persistedVersion?.id === saveSnapshot.id
|
|
563
|
+
? { ...saveSnapshot, updated: persistedVersion.updated }
|
|
564
|
+
: saveSnapshot;
|
|
565
|
+
return createDelayedSuggestedEditSave(snapshotForSave, persistedVersion?.id === saveSnapshot.id
|
|
566
|
+
? persistedVersion.updated
|
|
567
|
+
: undefined);
|
|
568
|
+
})
|
|
569
|
+
.then((result) => {
|
|
570
|
+
if (result.type === "success" && result.data) {
|
|
571
|
+
const updatedEdit = result.data;
|
|
572
|
+
if (updatedEdit.updated) {
|
|
573
|
+
persistedSuggestedEditVersions.set(fieldKey, {
|
|
574
|
+
id: updatedEdit.id,
|
|
575
|
+
updated: updatedEdit.updated,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (isSuggestedEditRemoved(updatedEdit.id)) {
|
|
579
|
+
void deleteSuggestedEdit(updatedEdit).then((deleteResult) => {
|
|
580
|
+
if (deleteResult.type !== "success") {
|
|
581
|
+
console.warn("Failed to delete reverted suggestion after save:", deleteResult.summary, deleteResult.details);
|
|
582
|
+
}
|
|
583
|
+
}, () => undefined);
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
stateRef.current.setSuggestedEdits((current) => upsertSuggestedEdit(current, updatedEdit, {
|
|
587
|
+
expectedNewValue,
|
|
588
|
+
source: "save-ack",
|
|
589
|
+
}));
|
|
590
|
+
}
|
|
591
|
+
else if (result.type === "conflict") {
|
|
592
|
+
if (result.suggestion) {
|
|
593
|
+
if (result.suggestion.updated) {
|
|
594
|
+
persistedSuggestedEditVersions.set(fieldKey, {
|
|
595
|
+
id: result.suggestion.id,
|
|
596
|
+
updated: result.suggestion.updated,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
stateRef.current.setSuggestedEdits((current) => upsertSuggestedEdit(current, result.suggestion, {
|
|
600
|
+
source: "save-ack",
|
|
601
|
+
}));
|
|
602
|
+
if (result.suggestion.status !== "pending" &&
|
|
603
|
+
pendingSuggestedEdits.get(pendingFieldKey)?.id ===
|
|
604
|
+
result.suggestion.id) {
|
|
605
|
+
pendingSuggestedEdits.delete(pendingFieldKey);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
else if (result.type === "error" ||
|
|
610
|
+
result.type === "unauthorized") {
|
|
611
|
+
if (!isSuggestedEditRemoved(saveSnapshot.id)) {
|
|
612
|
+
ui.showErrorToast({
|
|
613
|
+
summary: result.summary || "Failed to save suggestion",
|
|
614
|
+
details: result.details,
|
|
615
|
+
rawDetails: result.rawDetails,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return result;
|
|
620
|
+
})
|
|
621
|
+
.finally(() => {
|
|
622
|
+
const pendingEdit = pendingSuggestedEdits.get(pendingFieldKey);
|
|
623
|
+
if (pendingEdit?.id === saveSnapshot.id &&
|
|
624
|
+
pendingEdit.newValue === saveSnapshot.newValue) {
|
|
625
|
+
pendingSuggestedEdits.delete(pendingFieldKey);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
}, [ui]);
|
|
629
|
+
const editField = useCallback(async ({ field, value, rawValue, forceMode, refresh = "immediate", saveMode = "debounced", originatingSlotId, }) => {
|
|
552
630
|
const effectiveMode = forceMode ?? stateRef.current.mode;
|
|
553
631
|
fieldEditGeneration.current += 1;
|
|
632
|
+
const operationBoundary = fieldOperationBoundary.current;
|
|
554
633
|
if (effectiveMode === "suggestions" &&
|
|
555
634
|
(stateRef.current.page || stateRef.current.currentItemDescriptor)) {
|
|
556
635
|
// Create a unique key for this field to track pending saves
|
|
@@ -561,15 +640,24 @@ export function getOperationsContext(state, ui) {
|
|
|
561
640
|
fieldId: field.fieldId,
|
|
562
641
|
});
|
|
563
642
|
const pendingFieldKey = `${fieldKey}:${stateRef.current.user?.name || "unknown"}`;
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
643
|
+
let savePromise;
|
|
644
|
+
const op = await enqueueSuggestedEditFieldTask(`prepare:${pendingFieldKey}`, async () => {
|
|
645
|
+
const prepared = await getOrMergeSuggestedEditOp(field, rawValue, value, {
|
|
646
|
+
page: stateRef.current.page,
|
|
647
|
+
currentItemDescriptor: stateRef.current.currentItemDescriptor,
|
|
648
|
+
user: stateRef.current.user,
|
|
649
|
+
itemsRepository: itemsRepository,
|
|
650
|
+
comments: stateRef.current.comments,
|
|
651
|
+
commentsLoaded: stateRef.current.commentsLoaded,
|
|
652
|
+
suggestedEdits: stateRef.current.suggestedEdits,
|
|
653
|
+
setSuggestedEdits: stateRef.current.setSuggestedEdits,
|
|
654
|
+
});
|
|
655
|
+
// Register the save before releasing preparation. A rapid revert
|
|
656
|
+
// can now only queue its delete behind this create/update.
|
|
657
|
+
if (prepared) {
|
|
658
|
+
savePromise = queueSuggestedEditSave(fieldKey, pendingFieldKey, prepared);
|
|
659
|
+
}
|
|
660
|
+
return prepared;
|
|
573
661
|
});
|
|
574
662
|
if (op) {
|
|
575
663
|
logFieldValueDiagnostic("operations", "suggested edit prepared", {
|
|
@@ -581,93 +669,6 @@ export function getOperationsContext(state, ui) {
|
|
|
581
669
|
oldValue: summarizeFieldValue(op.oldValue),
|
|
582
670
|
newValue: summarizeFieldValue(op.newValue),
|
|
583
671
|
});
|
|
584
|
-
// Keep local suggestion state in step with typing immediately, then
|
|
585
|
-
// serialize persistence behind any in-flight save for this field.
|
|
586
|
-
// Create and track the save promise. useDebouncedCallback can return
|
|
587
|
-
// undefined before the trailing call runs, so this path uses a real
|
|
588
|
-
// promise that resolves after the delayed save completes.
|
|
589
|
-
const saveSnapshot = { ...op };
|
|
590
|
-
const expectedNewValue = saveSnapshot.newValue;
|
|
591
|
-
const savePromise = enqueueSuggestedEditFieldTask(fieldKey, () => {
|
|
592
|
-
// Input handlers may prepare several snapshots before React state
|
|
593
|
-
// receives the first save acknowledgement. Read the durable
|
|
594
|
-
// server-issued version at execution time so every queued save is
|
|
595
|
-
// based on the immediately preceding one.
|
|
596
|
-
const persistedVersion = persistedSuggestedEditVersions.get(fieldKey);
|
|
597
|
-
const snapshotForSave = persistedVersion?.id === saveSnapshot.id
|
|
598
|
-
? { ...saveSnapshot, updated: persistedVersion.updated }
|
|
599
|
-
: saveSnapshot;
|
|
600
|
-
return createDelayedSuggestedEditSave(snapshotForSave, persistedVersion?.id === saveSnapshot.id
|
|
601
|
-
? persistedVersion.updated
|
|
602
|
-
: undefined);
|
|
603
|
-
})
|
|
604
|
-
.then((result) => {
|
|
605
|
-
// Update the suggested edits state with the result from the server
|
|
606
|
-
if (result.type === "success" && result.data) {
|
|
607
|
-
const updatedEdit = result.data;
|
|
608
|
-
if (updatedEdit.updated) {
|
|
609
|
-
persistedSuggestedEditVersions.set(fieldKey, {
|
|
610
|
-
id: updatedEdit.id,
|
|
611
|
-
updated: updatedEdit.updated,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
if (isSuggestedEditRemoved(updatedEdit.id)) {
|
|
615
|
-
// Reverted to a no-op while this save was in flight; make sure
|
|
616
|
-
// it stays gone on the server and do not re-add it locally.
|
|
617
|
-
void deleteSuggestedEdit(updatedEdit).then((deleteResult) => {
|
|
618
|
-
if (deleteResult.type !== "success") {
|
|
619
|
-
console.warn("Failed to delete reverted suggestion after save:", deleteResult.summary, deleteResult.details);
|
|
620
|
-
}
|
|
621
|
-
}, () => undefined);
|
|
622
|
-
return result;
|
|
623
|
-
}
|
|
624
|
-
stateRef.current.setSuggestedEdits((current) => upsertSuggestedEdit(current, updatedEdit, {
|
|
625
|
-
expectedNewValue,
|
|
626
|
-
source: "save-ack",
|
|
627
|
-
}));
|
|
628
|
-
}
|
|
629
|
-
else if (result.type === "conflict") {
|
|
630
|
-
if (result.suggestion) {
|
|
631
|
-
if (result.suggestion.updated) {
|
|
632
|
-
persistedSuggestedEditVersions.set(fieldKey, {
|
|
633
|
-
id: result.suggestion.id,
|
|
634
|
-
updated: result.suggestion.updated,
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
stateRef.current.setSuggestedEdits((current) => upsertSuggestedEdit(current, result.suggestion, {
|
|
638
|
-
source: "save-ack",
|
|
639
|
-
}));
|
|
640
|
-
if (result.suggestion.status !== "pending" &&
|
|
641
|
-
pendingSuggestedEdits.get(pendingFieldKey)?.id ===
|
|
642
|
-
result.suggestion.id) {
|
|
643
|
-
pendingSuggestedEdits.delete(pendingFieldKey);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
else if (result.type === "error" ||
|
|
648
|
-
result.type === "unauthorized") {
|
|
649
|
-
// Show error toast for failed suggested edit save — unless the
|
|
650
|
-
// suggestion was reverted while the request was in flight, in
|
|
651
|
-
// which case there is nothing for the user to see or retry.
|
|
652
|
-
if (!isSuggestedEditRemoved(saveSnapshot.id)) {
|
|
653
|
-
ui.showErrorToast({
|
|
654
|
-
summary: result.summary || "Failed to save suggestion",
|
|
655
|
-
details: result.details,
|
|
656
|
-
rawDetails: result.rawDetails,
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
return result;
|
|
661
|
-
})
|
|
662
|
-
.finally(() => {
|
|
663
|
-
// Also remove from pending edits once this exact value is
|
|
664
|
-
// persisted. If the user typed again, keep the newer draft.
|
|
665
|
-
const pendingEdit = pendingSuggestedEdits.get(pendingFieldKey);
|
|
666
|
-
if (pendingEdit?.id === saveSnapshot.id &&
|
|
667
|
-
pendingEdit.newValue === saveSnapshot.newValue) {
|
|
668
|
-
pendingSuggestedEdits.delete(pendingFieldKey);
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
672
|
await savePromise;
|
|
672
673
|
}
|
|
673
674
|
logFieldValueDiagnostic("operations", "suggestions edit complete", {
|
|
@@ -682,11 +683,12 @@ export function getOperationsContext(state, ui) {
|
|
|
682
683
|
name: "unknown",
|
|
683
684
|
ai: false,
|
|
684
685
|
agentId: undefined,
|
|
685
|
-
}, true, value, rawValue, fieldModificationStore);
|
|
686
|
-
if (refresh === "immediate") {
|
|
686
|
+
}, true, value, rawValue, fieldModificationStore, refresh !== "none");
|
|
687
|
+
if (refresh === "immediate" || saveMode === "immediate") {
|
|
687
688
|
logFieldValueDiagnostic("operations", "saving immediately", {
|
|
688
689
|
field: summarizeFieldDescriptor(field),
|
|
689
690
|
saveSequence,
|
|
691
|
+
refresh,
|
|
690
692
|
});
|
|
691
693
|
return enqueueFieldSave(field, () => editFieldImmediate({
|
|
692
694
|
field,
|
|
@@ -695,6 +697,7 @@ export function getOperationsContext(state, ui) {
|
|
|
695
697
|
refresh,
|
|
696
698
|
saveSequence,
|
|
697
699
|
fieldModificationStore,
|
|
700
|
+
operationBoundary,
|
|
698
701
|
}));
|
|
699
702
|
}
|
|
700
703
|
const isNewField = lastEditField.current?.fieldId !== field.fieldId ||
|
|
@@ -713,44 +716,44 @@ export function getOperationsContext(state, ui) {
|
|
|
713
716
|
refresh,
|
|
714
717
|
saveSequence,
|
|
715
718
|
fieldModificationStore,
|
|
719
|
+
operationBoundary,
|
|
716
720
|
});
|
|
717
721
|
}, [
|
|
718
722
|
itemsRepository,
|
|
719
723
|
getOrMergeSuggestedEditOp,
|
|
724
|
+
queueSuggestedEditSave,
|
|
720
725
|
resolveOriginatingFieldModificationStore,
|
|
721
726
|
enqueueFieldSave,
|
|
722
727
|
]);
|
|
723
|
-
const editFieldImmediate = useCallback(async ({ field, value, rawValue, refresh = "immediate", saveSequence, fieldModificationStore, }) => {
|
|
728
|
+
const editFieldImmediate = useCallback(async ({ field, value, rawValue, refresh = "immediate", saveSequence, fieldModificationStore, operationBoundary, }) => {
|
|
724
729
|
const val = rawValue !== undefined ? rawValue : value;
|
|
725
730
|
if (val === undefined)
|
|
726
731
|
return;
|
|
727
|
-
const op = await getEditOP(field, rawValue, value);
|
|
728
|
-
//
|
|
729
|
-
//
|
|
732
|
+
const op = await getEditOP(field, rawValue, value, operationBoundary);
|
|
733
|
+
// An operation that could not be constructed was not saved. Keep the
|
|
734
|
+
// modification dirty instead of publishing a false successful state.
|
|
730
735
|
if (!op) {
|
|
731
|
-
|
|
736
|
+
const error = new Error(`Could not create edit operation for field ${field.fieldId}`);
|
|
737
|
+
console.warn("[editFieldImmediate]", error.message);
|
|
732
738
|
logFieldValueDiagnostic("operations", "editFieldImmediate no op", {
|
|
733
739
|
field: summarizeFieldDescriptor(field),
|
|
734
740
|
value: summarizeFieldValue(value),
|
|
735
741
|
rawValue: summarizeFieldValue(rawValue),
|
|
736
742
|
saveSequence,
|
|
737
743
|
});
|
|
738
|
-
|
|
739
|
-
return;
|
|
744
|
+
throw error;
|
|
740
745
|
}
|
|
741
746
|
try {
|
|
742
747
|
const result = await executeOp(op, { refresh });
|
|
743
748
|
// Fix: Check if the operation actually succeeded
|
|
744
749
|
// If result is undefined/null, the operation likely failed
|
|
745
750
|
if (!result) {
|
|
746
|
-
console.warn("[editFieldImmediate] Edit operation returned no result, clearing dirty state");
|
|
747
751
|
logFieldValueDiagnostic("operations", "editFieldImmediate no result", {
|
|
748
752
|
field: summarizeFieldDescriptor(field),
|
|
749
753
|
opId: op.id,
|
|
750
754
|
saveSequence,
|
|
751
755
|
});
|
|
752
|
-
|
|
753
|
-
return;
|
|
756
|
+
throw new Error(`Edit operation returned no result for field ${field.fieldId}`);
|
|
754
757
|
}
|
|
755
758
|
// Don't refresh here - the websocket handler will trigger a refresh when
|
|
756
759
|
// it receives the item-changed notification from the server. This avoids
|
|
@@ -759,15 +762,23 @@ export function getOperationsContext(state, ui) {
|
|
|
759
762
|
isWorkflowMetadataField(op.fieldId, op.fieldName)) {
|
|
760
763
|
refreshWorkboxAfterWorkflowChange();
|
|
761
764
|
}
|
|
762
|
-
if (result.executionStatus
|
|
763
|
-
|
|
764
|
-
completedFieldSaveGeneration.current += 1;
|
|
765
|
+
if (result.executionStatus === "failed") {
|
|
766
|
+
throw new Error(`Edit operation failed for field ${field.fieldId}`);
|
|
765
767
|
}
|
|
768
|
+
latestSavedFieldEditOperationId.current = op.id;
|
|
769
|
+
completedFieldSaveGeneration.current += 1;
|
|
766
770
|
itemsRepository.onFieldSaved(field, val, saveSequence || 0, fieldModificationStore);
|
|
771
|
+
if (refresh !== "none") {
|
|
772
|
+
// The edit response can precede rendering-host cache invalidation.
|
|
773
|
+
// The immediate refresh remains the fast path; this quiet follow-up
|
|
774
|
+
// reconciles the canvas when that first render observed stale data
|
|
775
|
+
// or the corresponding websocket notification was missed.
|
|
776
|
+
stateRef.current.requestRefresh("waitForQuietPeriod");
|
|
777
|
+
}
|
|
767
778
|
}
|
|
768
779
|
catch (error) {
|
|
769
|
-
//
|
|
770
|
-
//
|
|
780
|
+
// Network and execution errors leave the modification dirty: clearing
|
|
781
|
+
// it here makes consumers believe persistence completed successfully.
|
|
771
782
|
console.error("[editFieldImmediate] Error saving field:", error);
|
|
772
783
|
logFieldValueDiagnostic("operations", "editFieldImmediate failed", {
|
|
773
784
|
field: summarizeFieldDescriptor(field),
|
|
@@ -776,10 +787,10 @@ export function getOperationsContext(state, ui) {
|
|
|
776
787
|
saveSequence,
|
|
777
788
|
error: error instanceof Error ? error.message : String(error),
|
|
778
789
|
});
|
|
779
|
-
|
|
790
|
+
throw error;
|
|
780
791
|
}
|
|
781
792
|
}, [itemsRepository, executeOp, refreshWorkboxAfterWorkflowChange]);
|
|
782
|
-
const executeEditFieldDebounced = useDebouncedCallback(async ({ field, value, rawValue, refresh, saveSequence, fieldModificationStore, }) => {
|
|
793
|
+
const executeEditFieldDebounced = useDebouncedCallback(async ({ field, value, rawValue, refresh, saveSequence, fieldModificationStore, operationBoundary, }) => {
|
|
783
794
|
// Fix: Use the saveSequence passed from editField instead of calling
|
|
784
795
|
// updateFieldValue again. This prevents the race condition where the
|
|
785
796
|
// field gets marked dirty twice with different sequences.
|
|
@@ -790,6 +801,7 @@ export function getOperationsContext(state, ui) {
|
|
|
790
801
|
refresh,
|
|
791
802
|
saveSequence,
|
|
792
803
|
fieldModificationStore,
|
|
804
|
+
operationBoundary,
|
|
793
805
|
}));
|
|
794
806
|
}, stateRef.current.configuration.debounceFieldEditsInterval * 2, { trailing: true });
|
|
795
807
|
const flushPendingFieldEdits = useCallback(async () => {
|
|
@@ -806,8 +818,20 @@ export function getOperationsContext(state, ui) {
|
|
|
806
818
|
? latestSavedFieldEditOperationId.current
|
|
807
819
|
: undefined;
|
|
808
820
|
}, [executeEditFieldDebounced]);
|
|
821
|
+
const prepareForModeChange = useCallback(async () => {
|
|
822
|
+
const lastField = lastEditField.current;
|
|
823
|
+
await flushPendingFieldEdits();
|
|
824
|
+
if (!lastField)
|
|
825
|
+
return;
|
|
826
|
+
// A mode switch is a consistency boundary. Refresh the field that was just
|
|
827
|
+
// persisted so suggestions are based on the committed Sitecore value, not
|
|
828
|
+
// on an older cache response that raced the save acknowledgement.
|
|
829
|
+
await itemsRepository.refreshItems([lastField.item], {
|
|
830
|
+
fields: [lastField.fieldId],
|
|
831
|
+
});
|
|
832
|
+
}, [flushPendingFieldEdits, itemsRepository]);
|
|
809
833
|
// Note: We intentionally keep a single save debounce window here.
|
|
810
|
-
async function getEditOP(field, rawValue, value) {
|
|
834
|
+
async function getEditOP(field, rawValue, value, operationBoundary) {
|
|
811
835
|
const item = await itemsRepository.getItem(field.item);
|
|
812
836
|
if (!item)
|
|
813
837
|
return;
|
|
@@ -836,7 +860,8 @@ export function getOperationsContext(state, ui) {
|
|
|
836
860
|
op.itemId !== item.id ||
|
|
837
861
|
op.fieldId !== field.fieldId ||
|
|
838
862
|
op.mainItem.language !== item.language ||
|
|
839
|
-
op.mainItem.version !== item.version
|
|
863
|
+
op.mainItem.version !== item.version ||
|
|
864
|
+
lastOpBoundary.current !== operationBoundary) {
|
|
840
865
|
// Use state.page's descriptor only if its language AND version match the field's
|
|
841
866
|
// to prevent race conditions when switching languages or versions quickly
|
|
842
867
|
const page = stateRef.current.page;
|
|
@@ -868,6 +893,7 @@ export function getOperationsContext(state, ui) {
|
|
|
868
893
|
if (firstEditInheritedComponent)
|
|
869
894
|
op.mainItem.language = stateRef.current.page.item.language;
|
|
870
895
|
lastOp.current = op;
|
|
896
|
+
lastOpBoundary.current = operationBoundary;
|
|
871
897
|
return op;
|
|
872
898
|
}
|
|
873
899
|
const executeUndoOperation = useCallback(async (ops) => {
|
|
@@ -876,7 +902,10 @@ export function getOperationsContext(state, ui) {
|
|
|
876
902
|
// This ensures the UI only changes when the operation truly completes
|
|
877
903
|
const operationWasRemoved = isMissingEditOperationResult(result);
|
|
878
904
|
if (!operationWasRemoved) {
|
|
879
|
-
|
|
905
|
+
// Reconcile the affected repository items before starting the canvas
|
|
906
|
+
// request below. Refreshing here would let a busy rendering host read
|
|
907
|
+
// the pre-undo snapshot and repaint stale content over the new value.
|
|
908
|
+
handleResult(result, { refresh: "none" });
|
|
880
909
|
}
|
|
881
910
|
if (result.type === "success") {
|
|
882
911
|
for (const executedOp of result.data) {
|
|
@@ -902,7 +931,6 @@ export function getOperationsContext(state, ui) {
|
|
|
902
931
|
// The targeted refresh via itemsRepository.refreshItems is not sufficient because:
|
|
903
932
|
// 1. When undoing add-component, the component no longer exists to refresh
|
|
904
933
|
// 2. When undoing remove-component, the component wasn't in cache
|
|
905
|
-
const hasDeleteOperations = true; // Always refresh after undo
|
|
906
934
|
const itemsToRefresh = result.data
|
|
907
935
|
.map((x) => {
|
|
908
936
|
if (x.type === "edit-field")
|
|
@@ -921,8 +949,8 @@ export function getOperationsContext(state, ui) {
|
|
|
921
949
|
.flat()
|
|
922
950
|
.filter((x) => x != null);
|
|
923
951
|
await itemsRepository.refreshItems(itemsToRefresh);
|
|
924
|
-
|
|
925
|
-
|
|
952
|
+
markNextBridgeRefreshAuthoritative();
|
|
953
|
+
stateRef.current.requestRefresh("immediate");
|
|
926
954
|
return true;
|
|
927
955
|
}
|
|
928
956
|
else {
|
|
@@ -1228,7 +1256,7 @@ export function getOperationsContext(state, ui) {
|
|
|
1228
1256
|
// which merges server responses + websocket updates while enforcing scope filtering.
|
|
1229
1257
|
const operationWasRemoved = isMissingEditOperationResult(result);
|
|
1230
1258
|
if (!operationWasRemoved) {
|
|
1231
|
-
handleResult(result, { refresh: "
|
|
1259
|
+
handleResult(result, { refresh: "none" });
|
|
1232
1260
|
}
|
|
1233
1261
|
if (result.type === "success") {
|
|
1234
1262
|
for (const executedOp of result.data) {
|
|
@@ -1264,6 +1292,8 @@ export function getOperationsContext(state, ui) {
|
|
|
1264
1292
|
})
|
|
1265
1293
|
.filter((x) => x != null);
|
|
1266
1294
|
await itemsRepository.refreshItems(itemsToRefresh);
|
|
1295
|
+
markNextBridgeRefreshAuthoritative();
|
|
1296
|
+
stateRef.current.requestRefresh("immediate");
|
|
1267
1297
|
return true;
|
|
1268
1298
|
}
|
|
1269
1299
|
else {
|
|
@@ -1558,6 +1588,24 @@ export function getOperationsContext(state, ui) {
|
|
|
1558
1588
|
return true;
|
|
1559
1589
|
}, [itemsRepository, executeOp, ui]);
|
|
1560
1590
|
const moveItems = useCallback(async (items, target, index) => {
|
|
1591
|
+
// Capture source parents before the background operation starts. The
|
|
1592
|
+
// terminal payload does not need to carry this UI-specific context, and
|
|
1593
|
+
// after completion the items already belong to the destination.
|
|
1594
|
+
let sourceParents = [];
|
|
1595
|
+
try {
|
|
1596
|
+
const sourceItems = await itemsRepository.getItems(items);
|
|
1597
|
+
sourceParents = sourceItems
|
|
1598
|
+
.filter((item) => !!item.parentId)
|
|
1599
|
+
.map((item) => ({
|
|
1600
|
+
id: item.parentId,
|
|
1601
|
+
language: item.language,
|
|
1602
|
+
version: 0,
|
|
1603
|
+
database: item.database,
|
|
1604
|
+
}));
|
|
1605
|
+
}
|
|
1606
|
+
catch (error) {
|
|
1607
|
+
console.warn("Could not capture move source parents:", error);
|
|
1608
|
+
}
|
|
1561
1609
|
// Use generic edit endpoint for long-running operation support
|
|
1562
1610
|
const op = {
|
|
1563
1611
|
type: "move-items",
|
|
@@ -1569,9 +1617,29 @@ export function getOperationsContext(state, ui) {
|
|
|
1569
1617
|
id: uuid(),
|
|
1570
1618
|
title: `Moving ${items.length} item(s)...`,
|
|
1571
1619
|
};
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1620
|
+
let result = (await executeOp(op));
|
|
1621
|
+
if (copyItemsNeedsTerminalCompletion(result)) {
|
|
1622
|
+
try {
|
|
1623
|
+
const terminal = await waitForEditOperationTerminal(result.id, 120_000);
|
|
1624
|
+
result = { ...result, ...terminal };
|
|
1625
|
+
}
|
|
1626
|
+
catch {
|
|
1627
|
+
return false;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
if (result.executionStatus === "failed" || result.errorMessage) {
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
const parentsById = new Map();
|
|
1634
|
+
for (const parent of [...sourceParents, { ...target, version: 0 }]) {
|
|
1635
|
+
const key = cleanId(parent.id) ?? parent.id;
|
|
1636
|
+
parentsById.set(key, parent);
|
|
1637
|
+
}
|
|
1638
|
+
await itemsRepository.refreshItems(Array.from(parentsById.values()), {
|
|
1639
|
+
children: true,
|
|
1640
|
+
});
|
|
1641
|
+
return true;
|
|
1642
|
+
}, [executeOp, itemsRepository]);
|
|
1575
1643
|
const createItem = useCallback(async (parent, templateId, name) => {
|
|
1576
1644
|
const op = {
|
|
1577
1645
|
type: "create-item",
|
|
@@ -1736,24 +1804,20 @@ export function getOperationsContext(state, ui) {
|
|
|
1736
1804
|
}));
|
|
1737
1805
|
return result;
|
|
1738
1806
|
}, [executeOp]);
|
|
1739
|
-
const onFieldBlur = useCallback(() => {
|
|
1807
|
+
const onFieldBlur = useCallback(async () => {
|
|
1740
1808
|
const editGenerationAtBlur = fieldEditGeneration.current;
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
// Reset the last operation to force creation of a new operation
|
|
1754
|
-
// when the user returns to edit any field after losing focus.
|
|
1755
|
-
lastOp.current = undefined;
|
|
1756
|
-
}, [executeEditFieldDebounced]);
|
|
1809
|
+
// Advance synchronously. The DOM blur event cannot await this callback, so
|
|
1810
|
+
// doing it after the save would allow an edit started during that save to
|
|
1811
|
+
// coalesce into the operation that belongs to the field being left.
|
|
1812
|
+
fieldOperationBoundary.current += 1;
|
|
1813
|
+
await flushPendingFieldEdits();
|
|
1814
|
+
// Preserve a newer edit that started while the blurred field was saving.
|
|
1815
|
+
// Otherwise force a new operation when the user returns to this field.
|
|
1816
|
+
if (fieldEditGeneration.current === editGenerationAtBlur) {
|
|
1817
|
+
lastOp.current = undefined;
|
|
1818
|
+
lastOpBoundary.current = undefined;
|
|
1819
|
+
}
|
|
1820
|
+
}, [flushPendingFieldEdits]);
|
|
1757
1821
|
// Extract state values to avoid object reference changes
|
|
1758
1822
|
const mode = stateRef.current.mode;
|
|
1759
1823
|
const page = stateRef.current.page;
|
|
@@ -1783,6 +1847,7 @@ export function getOperationsContext(state, ui) {
|
|
|
1783
1847
|
cloneItems,
|
|
1784
1848
|
duplicateItem,
|
|
1785
1849
|
onFieldBlur,
|
|
1850
|
+
prepareForModeChange,
|
|
1786
1851
|
}), [
|
|
1787
1852
|
addComponent,
|
|
1788
1853
|
ensureLock,
|
|
@@ -1810,6 +1875,7 @@ export function getOperationsContext(state, ui) {
|
|
|
1810
1875
|
cloneItems,
|
|
1811
1876
|
duplicateItem,
|
|
1812
1877
|
onFieldBlur,
|
|
1878
|
+
prepareForModeChange,
|
|
1813
1879
|
]);
|
|
1814
1880
|
// Cleanup function to flush pending debounced operations on unmount
|
|
1815
1881
|
// This prevents fields from getting stuck in "saving" state when navigating away
|
|
@@ -1924,10 +1990,10 @@ async function getOrMergeSuggestedEditOp(field, rawValue, value, state) {
|
|
|
1924
1990
|
// server cascade-deletes a suggestion's comments along with it).
|
|
1925
1991
|
state.commentsLoaded === true &&
|
|
1926
1992
|
shouldRemoveNoOpSuggestion(edit.oldValue, nextValue, () => getSuggestionCommentCount(edit, state)) &&
|
|
1927
|
-
//
|
|
1928
|
-
// field
|
|
1929
|
-
//
|
|
1930
|
-
!
|
|
1993
|
+
// A live value equal to the draft is an optimistic editor echo, not an
|
|
1994
|
+
// external field change. Preserve the suggestion only when the live field
|
|
1995
|
+
// contains a third value that differs from both its baseline and draft.
|
|
1996
|
+
!hasLiveFieldDivergedFromSuggestion(edit.oldValue, edit.newValue, fieldItem.rawValue);
|
|
1931
1997
|
// First check if there's a pending suggested edit for this field. A draft
|
|
1932
1998
|
// whose suggestion was already reverted must not be revived — a new edit
|
|
1933
1999
|
// mints a fresh suggestion instead.
|