@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.
Files changed (176) hide show
  1. package/dist/components/ui/LanguageSelector.js +4 -1
  2. package/dist/components/ui/LanguageSelector.js.map +1 -1
  3. package/dist/components/ui/select.js +12 -1
  4. package/dist/components/ui/select.js.map +1 -1
  5. package/dist/editor/FieldHistory.js +2 -2
  6. package/dist/editor/FieldHistory.js.map +1 -1
  7. package/dist/editor/ai/InlineAiDialog.js +35 -8
  8. package/dist/editor/ai/InlineAiDialog.js.map +1 -1
  9. package/dist/editor/ai/terminal/components/AgentProjectProgress.d.ts +4 -3
  10. package/dist/editor/ai/terminal/components/AgentProjectProgress.js +34 -50
  11. package/dist/editor/ai/terminal/components/AgentProjectProgress.js.map +1 -1
  12. package/dist/editor/ai/terminal/components/AgentTerminalView.d.ts +2 -1
  13. package/dist/editor/ai/terminal/components/AgentTerminalView.js +2 -2
  14. package/dist/editor/ai/terminal/components/AgentTerminalView.js.map +1 -1
  15. package/dist/editor/ai/terminal/useAgentTerminalController.js +1 -0
  16. package/dist/editor/ai/terminal/useAgentTerminalController.js.map +1 -1
  17. package/dist/editor/ai/terminal/useAgentTerminalViewProps.d.ts +1 -0
  18. package/dist/editor/ai/terminal/useAgentTerminalViewProps.js +2 -1
  19. package/dist/editor/ai/terminal/useAgentTerminalViewProps.js.map +1 -1
  20. package/dist/editor/bridge/protocol.d.ts +4 -0
  21. package/dist/editor/bridge/protocol.js +1 -0
  22. package/dist/editor/bridge/protocol.js.map +1 -1
  23. package/dist/editor/client/EditorShell.js +174 -65
  24. package/dist/editor/client/EditorShell.js.map +1 -1
  25. package/dist/editor/client/editContext.d.ts +13 -3
  26. package/dist/editor/client/editContext.js.map +1 -1
  27. package/dist/editor/client/hooks/activeSessionSnapshots.d.ts +2 -0
  28. package/dist/editor/client/hooks/activeSessionSnapshots.js +43 -0
  29. package/dist/editor/client/hooks/activeSessionSnapshots.js.map +1 -0
  30. package/dist/editor/client/hooks/feedbackSocketState.js +3 -0
  31. package/dist/editor/client/hooks/feedbackSocketState.js.map +1 -1
  32. package/dist/editor/client/hooks/operationRefreshDeduplication.d.ts +22 -0
  33. package/dist/editor/client/hooks/operationRefreshDeduplication.js +45 -0
  34. package/dist/editor/client/hooks/operationRefreshDeduplication.js.map +1 -0
  35. package/dist/editor/client/hooks/socketMessageRetention.d.ts +7 -0
  36. package/dist/editor/client/hooks/socketMessageRetention.js +11 -0
  37. package/dist/editor/client/hooks/socketMessageRetention.js.map +1 -0
  38. package/dist/editor/client/hooks/useSocketMessageHandler.js +39 -19
  39. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
  40. package/dist/editor/client/hooks/useWorkbox.d.ts +4 -0
  41. package/dist/editor/client/hooks/useWorkbox.js +132 -2
  42. package/dist/editor/client/hooks/useWorkbox.js.map +1 -1
  43. package/dist/editor/client/itemsRepository.d.ts +7 -1
  44. package/dist/editor/client/itemsRepository.js +41 -18
  45. package/dist/editor/client/itemsRepository.js.map +1 -1
  46. package/dist/editor/client/operations.d.ts +7 -3
  47. package/dist/editor/client/operations.js +216 -150
  48. package/dist/editor/client/operations.js.map +1 -1
  49. package/dist/editor/client/pageModelBuilder.js +15 -3
  50. package/dist/editor/client/pageModelBuilder.js.map +1 -1
  51. package/dist/editor/client/sidebarAvailability.d.ts +4 -0
  52. package/dist/editor/client/sidebarAvailability.js +15 -0
  53. package/dist/editor/client/sidebarAvailability.js.map +1 -1
  54. package/dist/editor/commands/itemCommands.js +1 -1
  55. package/dist/editor/commands/itemCommands.js.map +1 -1
  56. package/dist/editor/field-types/RichTextEditorComponent.js +6 -1
  57. package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
  58. package/dist/editor/field-types/SingleLineText.js +2 -3
  59. package/dist/editor/field-types/SingleLineText.js.map +1 -1
  60. package/dist/editor/field-types/richtext/components/ReactSlate.d.ts +6 -1
  61. package/dist/editor/field-types/richtext/components/ReactSlate.js +91 -5
  62. package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
  63. package/dist/editor/field-types/singleLineTextValue.d.ts +3 -0
  64. package/dist/editor/field-types/singleLineTextValue.js +9 -0
  65. package/dist/editor/field-types/singleLineTextValue.js.map +1 -0
  66. package/dist/editor/menubar/ItemLanguageVersion.js +1 -1
  67. package/dist/editor/menubar/ItemLanguageVersion.js.map +1 -1
  68. package/dist/editor/menubar/VersionSelector.js +18 -6
  69. package/dist/editor/menubar/VersionSelector.js.map +1 -1
  70. package/dist/editor/menubar/WorkflowButton.js +8 -3
  71. package/dist/editor/menubar/WorkflowButton.js.map +1 -1
  72. package/dist/editor/menubar/toolbar-sections/EditControls.js +1 -1
  73. package/dist/editor/menubar/toolbar-sections/EditControls.js.map +1 -1
  74. package/dist/editor/notifications/NotificationCenter.js +26 -7
  75. package/dist/editor/notifications/NotificationCenter.js.map +1 -1
  76. package/dist/editor/notifications/useNotifications.d.ts +3 -1
  77. package/dist/editor/notifications/useNotifications.js +27 -9
  78. package/dist/editor/notifications/useNotifications.js.map +1 -1
  79. package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.js +18 -0
  80. package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.js.map +1 -1
  81. package/dist/editor/page-editor-chrome/InlineFieldHistoryButton.js +30 -8
  82. package/dist/editor/page-editor-chrome/InlineFieldHistoryButton.js.map +1 -1
  83. package/dist/editor/page-editor-chrome/VersionDiffHighlightings.js +30 -8
  84. package/dist/editor/page-editor-chrome/VersionDiffHighlightings.js.map +1 -1
  85. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.d.ts +5 -3
  86. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js +111 -19
  87. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js.map +1 -1
  88. package/dist/editor/page-viewer/EditorForm.d.ts +2 -1
  89. package/dist/editor/page-viewer/EditorForm.js +41 -12
  90. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  91. package/dist/editor/page-viewer/PageViewer.d.ts +2 -0
  92. package/dist/editor/page-viewer/PageViewer.js +11 -2
  93. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  94. package/dist/editor/page-viewer/PageViewerFrame.js +237 -42
  95. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  96. package/dist/editor/page-viewer/bridgeInlineAiSelection.d.ts +17 -0
  97. package/dist/editor/page-viewer/bridgeInlineAiSelection.js +80 -0
  98. package/dist/editor/page-viewer/bridgeInlineAiSelection.js.map +1 -0
  99. package/dist/editor/page-viewer/bridgeInlineEditCommit.d.ts +18 -0
  100. package/dist/editor/page-viewer/bridgeInlineEditCommit.js +24 -0
  101. package/dist/editor/page-viewer/bridgeInlineEditCommit.js.map +1 -0
  102. package/dist/editor/page-viewer/bridgeInlineEditSession.d.ts +3 -0
  103. package/dist/editor/page-viewer/bridgeInlineEditSession.js +30 -0
  104. package/dist/editor/page-viewer/bridgeInlineEditSession.js.map +1 -1
  105. package/dist/editor/page-viewer/bridgeItemChangeTarget.d.ts +9 -0
  106. package/dist/editor/page-viewer/bridgeItemChangeTarget.js +38 -0
  107. package/dist/editor/page-viewer/bridgeItemChangeTarget.js.map +1 -0
  108. package/dist/editor/page-viewer/bridgePointerDragRelease.d.ts +31 -0
  109. package/dist/editor/page-viewer/bridgePointerDragRelease.js +32 -0
  110. package/dist/editor/page-viewer/bridgePointerDragRelease.js.map +1 -0
  111. package/dist/editor/page-viewer/renderUrl.d.ts +8 -0
  112. package/dist/editor/page-viewer/renderUrl.js +10 -0
  113. package/dist/editor/page-viewer/renderUrl.js.map +1 -1
  114. package/dist/editor/personalization/PersonalizationPanel.js +52 -25
  115. package/dist/editor/personalization/PersonalizationPanel.js.map +1 -1
  116. package/dist/editor/reviews/Comment.js +3 -1
  117. package/dist/editor/reviews/Comment.js.map +1 -1
  118. package/dist/editor/reviews/CommentDisplayPopover.js +2 -0
  119. package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
  120. package/dist/editor/reviews/Comments.js +16 -3
  121. package/dist/editor/reviews/Comments.js.map +1 -1
  122. package/dist/editor/reviews/SuggestedEdit.js +25 -2
  123. package/dist/editor/reviews/SuggestedEdit.js.map +1 -1
  124. package/dist/editor/reviews/SuggestionCommentThread.js +2 -0
  125. package/dist/editor/reviews/SuggestionCommentThread.js.map +1 -1
  126. package/dist/editor/reviews/applySuggestedEditActions.js +105 -25
  127. package/dist/editor/reviews/applySuggestedEditActions.js.map +1 -1
  128. package/dist/editor/reviews/commentState.d.ts +4 -0
  129. package/dist/editor/reviews/commentState.js +15 -0
  130. package/dist/editor/reviews/commentState.js.map +1 -0
  131. package/dist/editor/reviews/feedbackOrdering.d.ts +1 -0
  132. package/dist/editor/reviews/feedbackOrdering.js +6 -0
  133. package/dist/editor/reviews/feedbackOrdering.js.map +1 -1
  134. package/dist/editor/reviews/suggestionContentChange.d.ts +7 -0
  135. package/dist/editor/reviews/suggestionContentChange.js +10 -0
  136. package/dist/editor/reviews/suggestionContentChange.js.map +1 -1
  137. package/dist/editor/reviews/suggestionPatch.js +6 -0
  138. package/dist/editor/reviews/suggestionPatch.js.map +1 -1
  139. package/dist/editor/services/personalizationService.js +30 -6
  140. package/dist/editor/services/personalizationService.js.map +1 -1
  141. package/dist/editor/services/suggestedEditsService.d.ts +1 -0
  142. package/dist/editor/services/suggestedEditsService.js +3 -0
  143. package/dist/editor/services/suggestedEditsService.js.map +1 -1
  144. package/dist/editor/settings/SettingsView.js +15 -7
  145. package/dist/editor/settings/SettingsView.js.map +1 -1
  146. package/dist/editor/settings/panels/MemoriesPanel.js +3 -2
  147. package/dist/editor/settings/panels/MemoriesPanel.js.map +1 -1
  148. package/dist/editor/settings/panels/ProjectTemplatesPanel.d.ts +3 -0
  149. package/dist/editor/settings/panels/ProjectTemplatesPanel.js +84 -15
  150. package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -1
  151. package/dist/editor/settings/panels/projectTemplateDuplication.d.ts +5 -0
  152. package/dist/editor/settings/panels/projectTemplateDuplication.js +52 -0
  153. package/dist/editor/settings/panels/projectTemplateDuplication.js.map +1 -0
  154. package/dist/editor/ui/PerfectTree.js +2 -2
  155. package/dist/editor/ui/PerfectTree.js.map +1 -1
  156. package/dist/editor/ui/PublishRestrictionsDialog.js +9 -1
  157. package/dist/editor/ui/PublishRestrictionsDialog.js.map +1 -1
  158. package/dist/editor/utils.js +5 -2
  159. package/dist/editor/utils.js.map +1 -1
  160. package/dist/editor/views/EditorSlot.js +5 -1
  161. package/dist/editor/views/EditorSlot.js.map +1 -1
  162. package/dist/editor/views/SingleEditView.d.ts +4 -1
  163. package/dist/editor/views/SingleEditView.js +2 -2
  164. package/dist/editor/views/SingleEditView.js.map +1 -1
  165. package/dist/licensing/LicenseContext.js +7 -0
  166. package/dist/licensing/LicenseContext.js.map +1 -1
  167. package/dist/revision.d.ts +2 -2
  168. package/dist/revision.js +2 -2
  169. package/dist/task-board/TaskBoardWorkspace.js +5 -1
  170. package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
  171. package/dist/task-board/taskExecutionRecords.js +7 -0
  172. package/dist/task-board/taskExecutionRecords.js.map +1 -1
  173. package/dist/task-board/views/DependencyGraphView.d.ts +1 -0
  174. package/dist/task-board/views/DependencyGraphView.js +15 -12
  175. package/dist/task-board/views/DependencyGraphView.js.map +1 -1
  176. package/package.json +1 -1
@@ -13,7 +13,7 @@ import { showExpandableErrorToast } from "./errorToast";
13
13
  import { isRedoStackEntry, reconcileNewHistoryOperation, } from "./editHistoryReconciliation";
14
14
  import { isManualShowMeFocusTransfer, isManualShowMeTarget, } from "./manualShowMeFocus";
15
15
  import { isAgentTerminalFocusTransfer, nextAgentTerminalInteractionAt, } from "./agentTerminalFocus";
16
- import { getAvailableSidebarsForWorkspace } from "./sidebarAvailability";
16
+ import { getAvailableSidebarsForWorkspace, parseSidebarUrlValue, sidebarUrlValue, } from "./sidebarAvailability";
17
17
  import { executeFieldAction as executeFieldServerAction, connectSocket, getEditHistory, getRunningOperations, reconnectSession, releaseFieldLocks, validateItems, } from "../services/editService";
18
18
  import { emitAgentDocumentsChanged } from "../services/agentService";
19
19
  import { onSocketReconnect as onAgentSocketReconnect } from "../services/agentSubscriptionRegistry";
@@ -37,6 +37,7 @@ import { EditorFormHintPopover } from "../page-viewer/EditorFormHintPopover";
37
37
  import { FieldEditorPopup } from "../FieldEditorPopup";
38
38
  import { ConcurrentUserLimitDialog } from "../ConcurrentUserLimitDialog";
39
39
  import { SessionExpiredOverlay } from "./SessionExpiredOverlay";
40
+ import { filterRemovedComments } from "../reviews/commentState";
40
41
  import { post } from "../services/serviceHelper";
41
42
  import { CONTENT_TREE_SIDEBAR_ID } from "../sidebar/contentTreeConstants";
42
43
  import { PageViewerFrame } from "../page-viewer/PageViewerFrame";
@@ -89,14 +90,9 @@ function getGlobalHistoryLoadMoreIncrement(filterBySession) {
89
90
  ? GLOBAL_SESSION_HISTORY_LOAD_MORE_INCREMENT
90
91
  : GLOBAL_HISTORY_LOAD_MORE_INCREMENT;
91
92
  }
92
- // Sentinel written to the `sidebar` URL param when the user has explicitly closed
93
- // every sidebar. Distinguishes "no preference yet" (param absent) from "user wants
94
- // nothing open" (param present with this value) so reload can honor the user's intent.
95
- const SIDEBAR_NONE_SENTINEL = "none";
96
93
  const PARHELIA_SETTINGS_ITEM_ID = "a2f3c168bb2841ff9673ee7f3cc624af";
97
94
  const PARHELIA_SETTINGS_ITEM_PATH = "/sitecore/system/parhelia/settings";
98
95
  const PARHELIA_SHOW_MINIMAP_FIELD_ID = "e75976d2179e4578b4140147b7525fa5";
99
- const sidebarUrlValue = (ids) => ids.length ? ids.join(",") : SIDEBAR_NONE_SENTINEL;
100
96
  // moved to hooks/useQuota
101
97
  function isParheliaSettingsSnapshotChange(changes) {
102
98
  return changes.some((change) => {
@@ -373,17 +369,8 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
373
369
  // Sidebar state
374
370
  const [openSidebars, setOpenSidebars] = useState(() => {
375
371
  const sidebarParam = searchParams.get("sidebar");
376
- let sidebars = [];
377
- // Presence check (not truthiness) so the sentinel "none" is respected across reloads.
378
- if (sidebarParam !== null) {
379
- sidebars =
380
- sidebarParam === SIDEBAR_NONE_SENTINEL
381
- ? []
382
- : sidebarParam.split(",").filter(Boolean);
383
- }
384
- else {
385
- sidebars = getInitialOpenSidebars(configuration, initialWorkspaceId);
386
- }
372
+ let sidebars = parseSidebarUrlValue(sidebarParam) ??
373
+ getInitialOpenSidebars(configuration, initialWorkspaceId);
387
374
  // If workspace=reviews was requested, ensure reviews sidebar is open
388
375
  if (isReviewsSidebarRequest && !sidebars.includes("reviews")) {
389
376
  sidebars.push("reviews");
@@ -643,6 +630,9 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
643
630
  const [itemLanguages, setItemLanguages] = useState([]);
644
631
  const [itemVersions, setItemVersions] = useState([]);
645
632
  const [isLoadingVersions, setIsLoadingVersions] = useState(false);
633
+ const versionLoadRequestRef = useRef(0);
634
+ const visibleVersionLoadRef = useRef(0);
635
+ const versionLoadsInFlightRef = useRef(new Map());
646
636
  const [modifiedFields, setModifiedFields] = useState([]);
647
637
  useEffect(() => {
648
638
  const stores = getFieldModificationStores();
@@ -747,8 +737,16 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
747
737
  const [mode, setModeState] = useState("edit");
748
738
  const modeRef = useRef("edit");
749
739
  modeRef.current = mode;
740
+ const modeTransitionChainRef = useRef(Promise.resolve());
750
741
  const modeChangeParticipantsRef = useRef(new Set());
751
- const loadSuggestedEditsRef = useRef(async () => { });
742
+ const loadSuggestedEditsRef = useRef(async () => undefined);
743
+ const suggestedEditsLoadRequestRef = useRef(0);
744
+ const suggestedEditsAppliedRequestRef = useRef(0);
745
+ const suggestedEditsItemKey = item
746
+ ? `${item.descriptor.id}:${item.descriptor.language}:${item.descriptor.version}`
747
+ : undefined;
748
+ const suggestedEditsItemKeyRef = useRef(suggestedEditsItemKey);
749
+ suggestedEditsItemKeyRef.current = suggestedEditsItemKey;
752
750
  const [modeTransitionRevision, setModeTransitionRevision] = useState(0);
753
751
  const [previewDate, setPreviewDate] = useState();
754
752
  const [layoutMode, setLayoutMode] = useState("final");
@@ -763,12 +761,23 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
763
761
  if (nextMode === previousMode)
764
762
  return;
765
763
  const participants = Array.from(modeChangeParticipantsRef.current);
766
- for (const participant of participants) {
767
- await participant.beforeModeChange?.(nextMode, previousMode, options);
768
- }
769
- for (const participant of participants) {
770
- participant.clearInlineDedupe?.(nextMode, previousMode, options);
771
- }
764
+ // Start event-time capture while the current mode and its live iframe
765
+ // editing session still exist. These promises are awaited in the
766
+ // serialized transition below, but invoking them here prevents the mode
767
+ // render from making an uncommitted DOM value unreachable.
768
+ const captureBarriers = participants.map((participant) => {
769
+ try {
770
+ return Promise.resolve(participant.captureBeforeModeChange?.(nextMode, previousMode, options));
771
+ }
772
+ catch (error) {
773
+ return Promise.reject(error);
774
+ }
775
+ });
776
+ // Reflect the user's requested mode immediately. Persistence barriers
777
+ // still drain in order below, but keeping the old mode visible until a
778
+ // slow host/save round-trip finishes invites duplicate toggle actions
779
+ // and makes the editor appear unresponsive. Inline edit sessions retain
780
+ // modeAtStart, so the in-flight value keeps its original routing.
772
781
  flushSync(() => {
773
782
  modeRef.current = nextMode;
774
783
  setModeState(nextMode);
@@ -782,9 +791,22 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
782
791
  setShowComments(true);
783
792
  }
784
793
  });
785
- if (nextMode === "suggestions") {
786
- void loadSuggestedEditsRef.current();
787
- }
794
+ const transition = modeTransitionChainRef.current
795
+ .catch(() => undefined)
796
+ .then(async () => {
797
+ await Promise.all(captureBarriers);
798
+ for (const participant of participants) {
799
+ await participant.beforeModeChange?.(nextMode, previousMode, options);
800
+ }
801
+ for (const participant of participants) {
802
+ participant.clearInlineDedupe?.(nextMode, previousMode, options);
803
+ }
804
+ if (nextMode === "suggestions" && modeRef.current === nextMode) {
805
+ await loadSuggestedEditsRef.current();
806
+ }
807
+ });
808
+ modeTransitionChainRef.current = transition;
809
+ await transition;
788
810
  }, []);
789
811
  const setMode = useCallback((nextMode) => {
790
812
  const resolvedMode = typeof nextMode === "function" ? nextMode(modeRef.current) : nextMode;
@@ -1714,11 +1736,8 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
1714
1736
  }
1715
1737
  // Handle sidebar changes — clear when absent so state matches the URL
1716
1738
  const sidebarParam = urlParams.get("sidebar");
1717
- const newSidebars = sidebarParam === SIDEBAR_NONE_SENTINEL
1718
- ? []
1719
- : sidebarParam
1720
- ? sidebarParam.split(",").filter(Boolean)
1721
- : [];
1739
+ const newSidebars = parseSidebarUrlValue(sidebarParam) ?? [];
1740
+ openSidebarsRef.current = newSidebars;
1722
1741
  setOpenSidebars(newSidebars);
1723
1742
  // Handle wizard ID changes
1724
1743
  const wizardId = urlParams.get("wizardid");
@@ -2010,7 +2029,7 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
2010
2029
  if (handleErrorResult(result, ui, state))
2011
2030
  return;
2012
2031
  setComments((x) => {
2013
- const loadedComments = result.data;
2032
+ const loadedComments = filterRemovedComments(result.data);
2014
2033
  const newComments = x.filter((c) => c.isNew &&
2015
2034
  c.mainItemId === currentItemDescriptor.id &&
2016
2035
  c.language === currentItemDescriptor.language &&
@@ -2024,20 +2043,34 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
2024
2043
  // Assuming currentItemDescriptor, ui, state, handleErrorResult, and setSuggestedEdits
2025
2044
  // are available in your component context.
2026
2045
  const loadSuggestedEdits = useCallback(async () => {
2046
+ const requestId = ++suggestedEditsLoadRequestRef.current;
2027
2047
  if (!item)
2028
- return;
2048
+ return undefined;
2049
+ const requestedItemKey = `${item.descriptor.id}:${item.descriptor.language}:${item.descriptor.version}`;
2029
2050
  const reviewId = searchParams.get("reviewId");
2030
2051
  const result = await getSuggestedEdits(item.descriptor.id, item.descriptor.language, item.descriptor.version, reviewId ?? undefined);
2052
+ // Item initialization, navigation, and realtime feedback notifications can
2053
+ // overlap. Accept the first response that can populate the current item,
2054
+ // then prevent older snapshots from replacing a newer one. This preserves
2055
+ // useful data while a second post-subscription refresh is still in flight.
2056
+ if (requestedItemKey !== suggestedEditsItemKeyRef.current)
2057
+ return undefined;
2058
+ if (requestId < suggestedEditsAppliedRequestRef.current) {
2059
+ return suggestedEditsRef.current;
2060
+ }
2031
2061
  if (handleErrorResult(result, ui, state))
2032
- return;
2062
+ return undefined;
2063
+ suggestedEditsAppliedRequestRef.current = requestId;
2033
2064
  // A refetch can race a locally reverted suggestion's server delete; don't
2034
2065
  // resurrect suggestions the user already removed.
2035
2066
  const edits = (result.data || []).filter((edit) => !isSuggestedEditRemoved(edit.id));
2036
2067
  setSuggestedEdits(edits);
2068
+ return edits;
2037
2069
  }, [item, searchParams]);
2038
- useEffect(() => {
2039
- loadSuggestedEditsRef.current = loadSuggestedEdits;
2040
- }, [loadSuggestedEdits]);
2070
+ // URL-driven mode initialization runs in an earlier effect. Update the ref
2071
+ // during render so entering suggestions on the first committed frame cannot
2072
+ // observe the initial no-op loader.
2073
+ loadSuggestedEditsRef.current = loadSuggestedEdits;
2041
2074
  const loadFavorites = useCallback(async () => {
2042
2075
  try {
2043
2076
  const result = await getAllFavorites();
@@ -2551,15 +2584,22 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
2551
2584
  return;
2552
2585
  // The previous item's comments don't speak for the new one.
2553
2586
  setCommentsLoaded(false);
2554
- // Loading feedback before client-info is acknowledged creates a gap: an
2555
- // update can arrive after this REST response but before the realtime page
2556
- // subscription exists. Once acknowledged, the snapshot and event stream
2557
- // form one continuous view of the server state.
2558
- if (!isClientInfoAcknowledged)
2559
- return;
2587
+ // Load an immediate snapshot so an existing comment or suggestion does not
2588
+ // depend on websocket registration latency. The acknowledgement dependency
2589
+ // deliberately runs this effect again after the page topic is subscribed;
2590
+ // that second snapshot closes the gap between the first response and the
2591
+ // realtime event stream.
2560
2592
  loadComments();
2561
2593
  loadSuggestedEdits();
2562
- }, [currentItemDescriptor, isClientInfoAcknowledged]);
2594
+ }, [
2595
+ currentItemDescriptor?.id,
2596
+ currentItemDescriptor?.language,
2597
+ currentItemDescriptor?.version,
2598
+ isClientInfoAcknowledged,
2599
+ item?.descriptor.id,
2600
+ item?.descriptor.language,
2601
+ item?.descriptor.version,
2602
+ ]);
2563
2603
  // Track the previous history mode to detect mode changes
2564
2604
  const prevHistoryModeRef = useRef(historyMode);
2565
2605
  const historyInitialLoadDoneRef = useRef(false);
@@ -3237,26 +3277,69 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
3237
3277
  useEffect(() => {
3238
3278
  loadItemVersions();
3239
3279
  }, [currentItemDescriptor]);
3240
- const loadItemVersions = useCallback(async () => {
3241
- if (!currentItemDescriptorRef.current) {
3242
- setItemVersions([]);
3243
- setItemLanguages([]);
3280
+ const loadItemVersions = useCallback(async (options) => {
3281
+ const descriptor = options?.descriptor
3282
+ ? getItemDescriptor(options.descriptor)
3283
+ : currentItemDescriptorRef.current
3284
+ ? getItemDescriptor(currentItemDescriptorRef.current)
3285
+ : undefined;
3286
+ if (!descriptor) {
3287
+ if (options?.clearOnMissing !== false) {
3288
+ setItemVersions([]);
3289
+ setItemLanguages([]);
3290
+ }
3244
3291
  setIsLoadingVersions(false);
3245
3292
  return;
3246
3293
  }
3294
+ const descriptorKey = [
3295
+ descriptor.database || "master",
3296
+ descriptor.id,
3297
+ descriptor.language,
3298
+ descriptor.version,
3299
+ ]
3300
+ .join(":")
3301
+ .toLowerCase();
3302
+ const requestId = ++versionLoadRequestRef.current;
3303
+ visibleVersionLoadRef.current = requestId;
3247
3304
  setIsLoadingVersions(true);
3248
3305
  try {
3249
- const result = await getLanguagesAndVersions(currentItemDescriptorRef.current, {
3250
- includePublishingRestrictions: true,
3251
- includePublishingTargets: true,
3252
- });
3253
- if (!result?.data) {
3254
- setItemVersions([]);
3255
- setItemLanguages([]);
3256
- showErrorToast({
3257
- summary: "Error",
3258
- details: "Failed to load item versions",
3306
+ let request = versionLoadsInFlightRef.current.get(descriptorKey);
3307
+ if (!request) {
3308
+ request = getLanguagesAndVersions(descriptor, {
3309
+ includePublishingRestrictions: true,
3310
+ includePublishingTargets: true,
3259
3311
  });
3312
+ versionLoadsInFlightRef.current.set(descriptorKey, request);
3313
+ const releaseRequest = () => {
3314
+ if (versionLoadsInFlightRef.current.get(descriptorKey) === request) {
3315
+ versionLoadsInFlightRef.current.delete(descriptorKey);
3316
+ }
3317
+ };
3318
+ void request.then(releaseRequest, releaseRequest);
3319
+ }
3320
+ const result = await request;
3321
+ const currentDescriptor = currentItemDescriptorRef.current;
3322
+ const currentDescriptorKey = currentDescriptor
3323
+ ? [
3324
+ currentDescriptor.database || "master",
3325
+ currentDescriptor.id,
3326
+ currentDescriptor.language,
3327
+ currentDescriptor.version,
3328
+ ]
3329
+ .join(":")
3330
+ .toLowerCase()
3331
+ : undefined;
3332
+ if (currentDescriptorKey !== descriptorKey)
3333
+ return;
3334
+ if (!result?.data) {
3335
+ if (!options?.preserveOnError) {
3336
+ setItemVersions([]);
3337
+ setItemLanguages([]);
3338
+ showErrorToast({
3339
+ summary: "Error",
3340
+ details: "Failed to load item versions",
3341
+ });
3342
+ }
3260
3343
  return;
3261
3344
  }
3262
3345
  const v = [...result.data.versions].reverse() || [];
@@ -3264,9 +3347,14 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
3264
3347
  setItemLanguages(result.data.languages);
3265
3348
  }
3266
3349
  finally {
3267
- setIsLoadingVersions(false);
3350
+ // A superseded request must not hide the spinner for the descriptor
3351
+ // that is currently loading. Duplicate calls for the same descriptor
3352
+ // share one network request and settle together.
3353
+ if (visibleVersionLoadRef.current === requestId) {
3354
+ setIsLoadingVersions(false);
3355
+ }
3268
3356
  }
3269
- }, [currentItemDescriptorRef.current, setItemVersions, setItemLanguages]);
3357
+ }, [setItemVersions, setItemLanguages]);
3270
3358
  const showErrorToast = useCallback((error) => {
3271
3359
  showExpandableErrorToast(error);
3272
3360
  }, []);
@@ -3912,6 +4000,23 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
3912
4000
  // workspace defaults are applied by the openSidebars state initializer, while
3913
4001
  // explicit sidebar URL state such as sidebar=none is still respected.
3914
4002
  const prevWorkspaceIdForDefaultsRef = useRef(null);
4003
+ // Next can retain this shell while hydrating or navigating within the same
4004
+ // route. Keep explicit sidebar URL state authoritative after the initializer
4005
+ // has run; the availability effect below still removes inaccessible panels.
4006
+ const sidebarParam = searchParams.get("sidebar");
4007
+ useEffect(() => {
4008
+ const requested = parseSidebarUrlValue(sidebarParam);
4009
+ if (requested === undefined)
4010
+ return;
4011
+ const current = openSidebarsRef.current;
4012
+ const unchanged = requested.length === current.length &&
4013
+ requested.every((id, index) => id === current[index]);
4014
+ if (unchanged)
4015
+ return;
4016
+ openSidebarsRef.current = requested;
4017
+ setOpenSidebars(requested);
4018
+ setSidebarStacks((prev) => normalizeSidebarStacks(requested, prev));
4019
+ }, [normalizeSidebarStacks, sidebarParam]);
3915
4020
  useEffect(() => {
3916
4021
  if (!currentWorkspace.supportsSidebars) {
3917
4022
  return;
@@ -3942,6 +4047,7 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
3942
4047
  currentWorkspace,
3943
4048
  getSidebarsForWorkspace,
3944
4049
  normalizeSidebarStacks,
4050
+ sidebarParam,
3945
4051
  updateUrl,
3946
4052
  workspaceId,
3947
4053
  ]);
@@ -4819,8 +4925,11 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
4819
4925
  // the drop target's parent here made the single-selection frame
4820
4926
  // FLIP-animate component -> parent -> component around every drop.
4821
4927
  let op;
4822
- if (!currentItemDescriptorRef.current)
4823
- return { committed: false };
4928
+ // A placeholder is rendered from this page model, so its insertion
4929
+ // must target the same item. The shell-level descriptor can briefly
4930
+ // lag while slots or a newly created page settle; using it here could
4931
+ // either discard a valid click or write the component to another page.
4932
+ const dropTargetDescriptor = page.item.descriptor;
4824
4933
  let insertionSequence;
4825
4934
  if ((spotPosition || spotPositionElement) && spotPositionAnchor) {
4826
4935
  insertionSequence = ++insertingSequenceRef.current;
@@ -4838,10 +4947,10 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
4838
4947
  // can exit cleanly and the editor header updates from "Add Component"
4839
4948
  // to the actual component name after the operation completes.
4840
4949
  setSelectedForInsertion("");
4841
- operationPromise = operations.addComponent(insertOption.typeId, placeholderKey, index, currentItemDescriptorRef.current, { showIndicator: false });
4950
+ operationPromise = operations.addComponent(insertOption.typeId, placeholderKey, index, dropTargetDescriptor, { showIndicator: false });
4842
4951
  }
4843
4952
  else if (droppedDragObject?.type == "template") {
4844
- operationPromise = operations.addComponent(droppedDragObject.typeId, placeholderKey, index, currentItemDescriptorRef.current, { showIndicator: false });
4953
+ operationPromise = operations.addComponent(droppedDragObject.typeId, placeholderKey, index, dropTargetDescriptor, { showIndicator: false });
4845
4954
  }
4846
4955
  else if (droppedDragObject?.type == "component" ||
4847
4956
  (droppedDragObject?.type == "items" && droppedDragObject.items)) {