@alpaca-editor/core 1.0.3942 → 1.0.3943

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 (308) hide show
  1. package/.prettierrc +3 -0
  2. package/build.css +3 -0
  3. package/components.json +21 -0
  4. package/dist/editor/ContentTree.d.ts +2 -1
  5. package/dist/editor/ContentTree.js +23 -21
  6. package/dist/editor/ContentTree.js.map +1 -1
  7. package/dist/editor/FieldActionsOverlay.js +0 -2
  8. package/dist/editor/FieldActionsOverlay.js.map +1 -1
  9. package/dist/editor/ScrollingContentTree.js +1 -1
  10. package/dist/editor/ScrollingContentTree.js.map +1 -1
  11. package/dist/editor/Titlebar.js +1 -1
  12. package/dist/editor/Titlebar.js.map +1 -1
  13. package/dist/editor/ai/GhostWriter.js +24 -3
  14. package/dist/editor/ai/GhostWriter.js.map +1 -1
  15. package/dist/editor/client/EditorClient.js +7 -7
  16. package/dist/editor/client/EditorClient.js.map +1 -1
  17. package/dist/editor/field-types/InternalLinkFieldEditor.js +60 -10
  18. package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
  19. package/dist/editor/media-selector/MediaFolderBrowser.js +48 -1
  20. package/dist/editor/media-selector/MediaFolderBrowser.js.map +1 -1
  21. package/dist/editor/menubar/PageSelector.js +116 -65
  22. package/dist/editor/menubar/PageSelector.js.map +1 -1
  23. package/dist/editor/page-viewer/EditorForm.js +5 -2
  24. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  25. package/dist/editor/ui/ItemSearch.js +14 -8
  26. package/dist/editor/ui/ItemSearch.js.map +1 -1
  27. package/dist/editor/ui/PerfectTree.d.ts +4 -2
  28. package/dist/editor/ui/PerfectTree.js +78 -4
  29. package/dist/editor/ui/PerfectTree.js.map +1 -1
  30. package/dist/editor/ui/Splitter.js +1 -1
  31. package/dist/revision.d.ts +2 -2
  32. package/dist/revision.js +2 -2
  33. package/dist/styles.css +8 -2
  34. package/eslint.config.mjs +4 -0
  35. package/images/bg-shape-black.webp +0 -0
  36. package/images/wizard-bg.png +0 -0
  37. package/images/wizard-tour.png +0 -0
  38. package/images/wizard.png +0 -0
  39. package/package.json +2 -8
  40. package/src/client-components/api.ts +6 -0
  41. package/src/client-components/index.ts +19 -0
  42. package/src/components/ActionButton.tsx +50 -0
  43. package/src/components/Error.tsx +57 -0
  44. package/src/components/ui/CardConnector.tsx +56 -0
  45. package/src/components/ui/button.tsx +62 -0
  46. package/src/components/ui/card.tsx +372 -0
  47. package/src/components/ui/context-menu.tsx +250 -0
  48. package/src/config/config.tsx +917 -0
  49. package/src/config/types.ts +286 -0
  50. package/src/editor/ComponentInfo.tsx +90 -0
  51. package/src/editor/ConfirmationDialog.tsx +103 -0
  52. package/src/editor/ContentTree.tsx +733 -0
  53. package/src/editor/ContextMenu.tsx +230 -0
  54. package/src/editor/Editor.tsx +90 -0
  55. package/src/editor/EditorWarning.tsx +34 -0
  56. package/src/editor/EditorWarnings.tsx +33 -0
  57. package/src/editor/FieldActionsOverlay.tsx +296 -0
  58. package/src/editor/FieldEditorPopup.tsx +65 -0
  59. package/src/editor/FieldHistory.tsx +75 -0
  60. package/src/editor/FieldList.tsx +190 -0
  61. package/src/editor/FieldListField.tsx +391 -0
  62. package/src/editor/FieldListFieldWithFallbacks.tsx +217 -0
  63. package/src/editor/FloatingToolbar.tsx +163 -0
  64. package/src/editor/ImageEditor.tsx +128 -0
  65. package/src/editor/ItemInfo.tsx +90 -0
  66. package/src/editor/LinkEditorDialog.tsx +196 -0
  67. package/src/editor/MainLayout.tsx +95 -0
  68. package/src/editor/MobileLayout.tsx +68 -0
  69. package/src/editor/NewEditorClient.tsx +11 -0
  70. package/src/editor/PictureCropper.tsx +568 -0
  71. package/src/editor/PictureEditor.tsx +301 -0
  72. package/src/editor/PictureEditorDialog.tsx +381 -0
  73. package/src/editor/PublishDialog.ignore +74 -0
  74. package/src/editor/ScrollingContentTree.tsx +68 -0
  75. package/src/editor/Terminal.tsx +227 -0
  76. package/src/editor/Titlebar.tsx +104 -0
  77. package/src/editor/ai/AiPopup.tsx +59 -0
  78. package/src/editor/ai/AiResponseMessage.tsx +106 -0
  79. package/src/editor/ai/AiTerminal.tsx +503 -0
  80. package/src/editor/ai/AiToolCall.tsx +61 -0
  81. package/src/editor/ai/EditorAiTerminal.tsx +20 -0
  82. package/src/editor/ai/GhostWriter.tsx +480 -0
  83. package/src/editor/ai/aiPageModel.ts +108 -0
  84. package/src/editor/ai/editorAiContext.ts +18 -0
  85. package/src/editor/client/AboutDialog.tsx +44 -0
  86. package/src/editor/client/EditorClient.tsx +2241 -0
  87. package/src/editor/client/GenericDialog.tsx +50 -0
  88. package/src/editor/client/editContext.ts +416 -0
  89. package/src/editor/client/helpers.ts +44 -0
  90. package/src/editor/client/itemsRepository.ts +574 -0
  91. package/src/editor/client/operations.ts +768 -0
  92. package/src/editor/client/pageModelBuilder.ts +219 -0
  93. package/src/editor/commands/commands.ts +22 -0
  94. package/src/editor/commands/componentCommands.tsx +431 -0
  95. package/src/editor/commands/createVersionCommand.ts +33 -0
  96. package/src/editor/commands/deleteVersionCommand.ts +71 -0
  97. package/src/editor/commands/itemCommands.tsx +351 -0
  98. package/src/editor/commands/localizeItem/LocalizeItemDialog.tsx +201 -0
  99. package/src/editor/commands/localizeItem/LocalizeItemUtils.ts +27 -0
  100. package/src/editor/commands/undo.ts +39 -0
  101. package/src/editor/component-designer/ComponentDesigner.tsx +70 -0
  102. package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx +11 -0
  103. package/src/editor/component-designer/ComponentDesignerMenu.tsx +91 -0
  104. package/src/editor/component-designer/ComponentEditor.tsx +97 -0
  105. package/src/editor/component-designer/ComponentRenderingCodeEditor.tsx +31 -0
  106. package/src/editor/component-designer/ComponentRenderingEditor.tsx +104 -0
  107. package/src/editor/component-designer/ComponentsDropdown.tsx +39 -0
  108. package/src/editor/component-designer/PlaceholdersEditor.tsx +179 -0
  109. package/src/editor/component-designer/RenderingsDropdown.tsx +36 -0
  110. package/src/editor/component-designer/TemplateEditor.tsx +236 -0
  111. package/src/editor/component-designer/aiContext.ts +23 -0
  112. package/src/editor/componentTreeHelper.tsx +116 -0
  113. package/src/editor/context-menu/CopyMoveMenu.tsx +103 -0
  114. package/src/editor/context-menu/InsertMenu.tsx +347 -0
  115. package/src/editor/control-center/About.tsx +342 -0
  116. package/src/editor/control-center/ControlCenterMenu.tsx +76 -0
  117. package/src/editor/control-center/IndexOverview.tsx +50 -0
  118. package/src/editor/control-center/IndexSettings.tsx +266 -0
  119. package/src/editor/control-center/Info.tsx +104 -0
  120. package/src/editor/control-center/QuotaInfo.tsx +301 -0
  121. package/src/editor/control-center/Status.tsx +113 -0
  122. package/src/editor/control-center/WebSocketMessages.tsx +155 -0
  123. package/src/editor/editor-warnings/ItemLocked.tsx +63 -0
  124. package/src/editor/editor-warnings/NoLanguageWriteAccess.tsx +22 -0
  125. package/src/editor/editor-warnings/NoWorkflowWriteAccess.tsx +23 -0
  126. package/src/editor/editor-warnings/NoWriteAccess.tsx +16 -0
  127. package/src/editor/editor-warnings/ValidationErrors.tsx +54 -0
  128. package/src/editor/field-types/AttachmentEditor.tsx +9 -0
  129. package/src/editor/field-types/CheckboxEditor.tsx +47 -0
  130. package/src/editor/field-types/DropLinkEditor.tsx +80 -0
  131. package/src/editor/field-types/DropListEditor.tsx +84 -0
  132. package/src/editor/field-types/ImageFieldEditor.tsx +65 -0
  133. package/src/editor/field-types/InternalLinkFieldEditor.tsx +188 -0
  134. package/src/editor/field-types/LinkFieldEditor.tsx +85 -0
  135. package/src/editor/field-types/MultiLineText.tsx +82 -0
  136. package/src/editor/field-types/PictureFieldEditor.tsx +121 -0
  137. package/src/editor/field-types/RawEditor.tsx +53 -0
  138. package/src/editor/field-types/ReactQuill.tsx +580 -0
  139. package/src/editor/field-types/RichTextEditor.tsx +22 -0
  140. package/src/editor/field-types/RichTextEditorComponent.tsx +127 -0
  141. package/src/editor/field-types/SingleLineText.tsx +174 -0
  142. package/src/editor/field-types/TreeListEditor.tsx +261 -0
  143. package/src/editor/fieldTypes.ts +140 -0
  144. package/src/editor/media-selector/AiImageSearch.tsx +185 -0
  145. package/src/editor/media-selector/AiImageSearchPrompt.tsx +94 -0
  146. package/src/editor/media-selector/MediaFolderBrowser.tsx +321 -0
  147. package/src/editor/media-selector/MediaSelector.tsx +42 -0
  148. package/src/editor/media-selector/Preview.tsx +14 -0
  149. package/src/editor/media-selector/Thumbnails.tsx +48 -0
  150. package/src/editor/media-selector/TreeSelector.tsx +292 -0
  151. package/src/editor/media-selector/UploadZone.tsx +137 -0
  152. package/src/editor/media-selector/index.ts +8 -0
  153. package/src/editor/menubar/ActionsMenu.tsx +94 -0
  154. package/src/editor/menubar/ActiveUsers.tsx +17 -0
  155. package/src/editor/menubar/ApproveAndPublish.tsx +18 -0
  156. package/src/editor/menubar/BrowseHistory.tsx +28 -0
  157. package/src/editor/menubar/ItemLanguageVersion.tsx +76 -0
  158. package/src/editor/menubar/LanguageSelector.tsx +226 -0
  159. package/src/editor/menubar/Menu.tsx +83 -0
  160. package/src/editor/menubar/NavButtons.tsx +74 -0
  161. package/src/editor/menubar/PageSelector.tsx +278 -0
  162. package/src/editor/menubar/PageViewerControls.tsx +120 -0
  163. package/src/editor/menubar/PreviewSecondaryControls.tsx +18 -0
  164. package/src/editor/menubar/SecondaryControls.tsx +45 -0
  165. package/src/editor/menubar/Separator.tsx +12 -0
  166. package/src/editor/menubar/SiteInfo.tsx +53 -0
  167. package/src/editor/menubar/User.tsx +27 -0
  168. package/src/editor/menubar/VersionSelector.tsx +142 -0
  169. package/src/editor/page-editor-chrome/CommentHighlighting.tsx +307 -0
  170. package/src/editor/page-editor-chrome/CommentHighlightings.tsx +35 -0
  171. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +59 -0
  172. package/src/editor/page-editor-chrome/FieldActionIndicators.tsx +23 -0
  173. package/src/editor/page-editor-chrome/FieldEditedIndicator.tsx +64 -0
  174. package/src/editor/page-editor-chrome/FieldEditedIndicators.tsx +35 -0
  175. package/src/editor/page-editor-chrome/FrameMenu.tsx +338 -0
  176. package/src/editor/page-editor-chrome/FrameMenus.tsx +48 -0
  177. package/src/editor/page-editor-chrome/InlineEditor.tsx +765 -0
  178. package/src/editor/page-editor-chrome/LockedFieldIndicator.tsx +61 -0
  179. package/src/editor/page-editor-chrome/NoLayout.tsx +36 -0
  180. package/src/editor/page-editor-chrome/PageEditorChrome.tsx +122 -0
  181. package/src/editor/page-editor-chrome/PictureEditorOverlay.tsx +161 -0
  182. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +169 -0
  183. package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +315 -0
  184. package/src/editor/page-editor-chrome/SuggestionHighlighting.tsx +300 -0
  185. package/src/editor/page-editor-chrome/SuggestionHighlightings.tsx +40 -0
  186. package/src/editor/page-editor-chrome/useInlineAICompletion.tsx +828 -0
  187. package/src/editor/page-viewer/DeviceToolbar.tsx +70 -0
  188. package/src/editor/page-viewer/EditorForm.tsx +262 -0
  189. package/src/editor/page-viewer/MiniMap.tsx +362 -0
  190. package/src/editor/page-viewer/PageViewer.tsx +169 -0
  191. package/src/editor/page-viewer/PageViewerFrame.tsx +1022 -0
  192. package/src/editor/page-viewer/pageModelSkeletonBuilder.ts +412 -0
  193. package/src/editor/page-viewer/pageViewContext.ts +186 -0
  194. package/src/editor/pageModel.ts +220 -0
  195. package/src/editor/picture-shared.tsx +53 -0
  196. package/src/editor/reviews/Comment.tsx +308 -0
  197. package/src/editor/reviews/Comments.tsx +125 -0
  198. package/src/editor/reviews/DiffView.tsx +109 -0
  199. package/src/editor/reviews/PreviewInfo.tsx +35 -0
  200. package/src/editor/reviews/Reviews.tsx +280 -0
  201. package/src/editor/reviews/SuggestedEdit.tsx +316 -0
  202. package/src/editor/reviews/reviewCommands.tsx +47 -0
  203. package/src/editor/reviews/useReviews.tsx +70 -0
  204. package/src/editor/services/aiService.ts +173 -0
  205. package/src/editor/services/componentDesignerService.ts +151 -0
  206. package/src/editor/services/contentService.ts +180 -0
  207. package/src/editor/services/editService.ts +488 -0
  208. package/src/editor/services/indexService.ts +24 -0
  209. package/src/editor/services/reviewsService.ts +53 -0
  210. package/src/editor/services/serviceHelper.ts +95 -0
  211. package/src/editor/services/suggestedEditsService.ts +39 -0
  212. package/src/editor/services/systemService.ts +5 -0
  213. package/src/editor/services/translationService.ts +21 -0
  214. package/src/editor/services-server/api.ts +150 -0
  215. package/src/editor/services-server/graphQL.ts +106 -0
  216. package/src/editor/sidebar/ComponentPalette.tsx +161 -0
  217. package/src/editor/sidebar/ComponentTree.tsx +549 -0
  218. package/src/editor/sidebar/Debug.tsx +111 -0
  219. package/src/editor/sidebar/DictionaryEditor.tsx +261 -0
  220. package/src/editor/sidebar/EditHistory.tsx +134 -0
  221. package/src/editor/sidebar/GraphQL.tsx +164 -0
  222. package/src/editor/sidebar/Insert.tsx +35 -0
  223. package/src/editor/sidebar/MainContentTree.tsx +102 -0
  224. package/src/editor/sidebar/Performance.tsx +53 -0
  225. package/src/editor/sidebar/Sessions.tsx +35 -0
  226. package/src/editor/sidebar/Sidebar.tsx +20 -0
  227. package/src/editor/sidebar/SidebarView.tsx +152 -0
  228. package/src/editor/sidebar/Translations.tsx +295 -0
  229. package/src/editor/sidebar/Validation.tsx +102 -0
  230. package/src/editor/sidebar/ViewSelector.tsx +60 -0
  231. package/src/editor/sidebar/Workbox.tsx +209 -0
  232. package/src/editor/ui/CenteredMessage.tsx +7 -0
  233. package/src/editor/ui/CopyMoveTargetSelectorDialog.tsx +81 -0
  234. package/src/editor/ui/CopyToClipboardButton.tsx +24 -0
  235. package/src/editor/ui/DialogButtons.tsx +11 -0
  236. package/src/editor/ui/Icons.tsx +709 -0
  237. package/src/editor/ui/ItemList.tsx +76 -0
  238. package/src/editor/ui/ItemNameDialogNew.tsx +118 -0
  239. package/src/editor/ui/ItemSearch.tsx +159 -0
  240. package/src/editor/ui/PerfectTree.tsx +676 -0
  241. package/src/editor/ui/Section.tsx +35 -0
  242. package/src/editor/ui/SimpleIconButton.tsx +54 -0
  243. package/src/editor/ui/SimpleMenu.tsx +40 -0
  244. package/src/editor/ui/SimpleTable.tsx +60 -0
  245. package/src/editor/ui/SimpleTabs.tsx +60 -0
  246. package/src/editor/ui/SimpleToolbar.tsx +7 -0
  247. package/src/editor/ui/Spinner.tsx +9 -0
  248. package/src/editor/ui/Splitter.tsx +420 -0
  249. package/src/editor/ui/StackedPanels.tsx +134 -0
  250. package/src/editor/ui/Toolbar.tsx +7 -0
  251. package/src/editor/utils/id-helper.ts +3 -0
  252. package/src/editor/utils/insertOptions.ts +69 -0
  253. package/src/editor/utils/itemutils.ts +29 -0
  254. package/src/editor/utils/useMemoDebug.ts +28 -0
  255. package/src/editor/utils.ts +486 -0
  256. package/src/editor/views/CompareView.tsx +245 -0
  257. package/src/editor/views/EditView.tsx +27 -0
  258. package/src/editor/views/ItemEditor.tsx +58 -0
  259. package/src/editor/views/MediaFolderEditView.tsx +66 -0
  260. package/src/editor/views/SingleEditView.tsx +57 -0
  261. package/src/fonts/Geist-Black.woff2 +0 -0
  262. package/src/fonts/Geist-Bold.woff2 +0 -0
  263. package/src/fonts/Geist-ExtraBold.woff2 +0 -0
  264. package/src/fonts/Geist-ExtraLight.woff2 +0 -0
  265. package/src/fonts/Geist-Light.woff2 +0 -0
  266. package/src/fonts/Geist-Medium.woff2 +0 -0
  267. package/src/fonts/Geist-Regular.woff2 +0 -0
  268. package/src/fonts/Geist-SemiBold.woff2 +0 -0
  269. package/src/fonts/Geist-Thin.woff2 +0 -0
  270. package/src/fonts/Geist[wght].woff2 +0 -0
  271. package/src/fonts/index.ts +10 -0
  272. package/src/index.ts +23 -0
  273. package/src/lib/safelist.tsx +16 -0
  274. package/src/lib/utils.ts +6 -0
  275. package/src/page-wizard/PageWizard.tsx +139 -0
  276. package/src/page-wizard/WizardBox.tsx +4 -0
  277. package/src/page-wizard/WizardBoxConnector.tsx +56 -0
  278. package/src/page-wizard/WizardSteps.tsx +458 -0
  279. package/src/page-wizard/service.ts +35 -0
  280. package/src/page-wizard/startPageWizardCommand.ts +26 -0
  281. package/src/page-wizard/steps/BuildPageStep.tsx +259 -0
  282. package/src/page-wizard/steps/CollectStep.tsx +296 -0
  283. package/src/page-wizard/steps/ComponentTypesSelector.tsx +454 -0
  284. package/src/page-wizard/steps/Components.tsx +193 -0
  285. package/src/page-wizard/steps/ContentStep.tsx +890 -0
  286. package/src/page-wizard/steps/EditButton.tsx +34 -0
  287. package/src/page-wizard/steps/FieldEditor.tsx +102 -0
  288. package/src/page-wizard/steps/Generate.tsx +60 -0
  289. package/src/page-wizard/steps/ImagesStep.tsx +382 -0
  290. package/src/page-wizard/steps/LayoutStep.tsx +227 -0
  291. package/src/page-wizard/steps/MetaDataStep.tsx +173 -0
  292. package/src/page-wizard/steps/SelectStep.tsx +281 -0
  293. package/src/page-wizard/steps/schema.ts +180 -0
  294. package/src/page-wizard/steps/usePageCreator.ts +325 -0
  295. package/src/page-wizard/usePageWizard.ts +79 -0
  296. package/src/revision.ts +2 -0
  297. package/src/splash-screen/NewPage.tsx +294 -0
  298. package/src/splash-screen/OpenPage.tsx +113 -0
  299. package/src/splash-screen/RecentPages.tsx +123 -0
  300. package/src/splash-screen/SectionHeadline.tsx +21 -0
  301. package/src/splash-screen/SplashScreen.tsx +195 -0
  302. package/src/tour/Tour.tsx +566 -0
  303. package/src/tour/default-tour.tsx +301 -0
  304. package/src/tour/preview-tour.tsx +128 -0
  305. package/src/types.ts +335 -0
  306. package/styles.css +765 -0
  307. package/tsconfig.build.json +31 -0
  308. package/tsconfig.json +14 -0
@@ -0,0 +1,2241 @@
1
+ "use client";
2
+
3
+ import React, {
4
+ useState,
5
+ useEffect,
6
+ useRef,
7
+ useCallback,
8
+ MouseEvent,
9
+ useMemo,
10
+ ReactNode,
11
+ } from "react";
12
+
13
+ import { Toast, ToastMessage } from "primereact/toast";
14
+
15
+ import {
16
+ EditContextProvider,
17
+ DragObject,
18
+ ModifiedFieldsContextProvider,
19
+ OperationsContextProvider,
20
+ ModifiedField,
21
+ EditContextType,
22
+ EditedField,
23
+ SelectionRange,
24
+ EditorMode,
25
+ } from "./editContext";
26
+
27
+ import type { OpenDialog } from "./editContext";
28
+
29
+ import { EditorConfiguration, MenuItem } from "../../config/types";
30
+ import { useRouter, useSearchParams, usePathname } from "next/navigation";
31
+ import { findComponent, getComponentById } from "../componentTreeHelper";
32
+
33
+ import { getOperationsContext } from "./operations";
34
+ import { handleErrorResult } from "./helpers";
35
+
36
+ import {
37
+ executeFieldAction as executeFieldServerAction,
38
+ connectSocket,
39
+ getEditHistory,
40
+ releaseFieldLocks,
41
+ validateItems,
42
+ } from "../services/editService";
43
+
44
+ import "primeicons/primeicons.css";
45
+ import "primereact/resources/themes/md-light-indigo/theme.css";
46
+ import "react-json-view-lite/dist/index.css";
47
+ import {
48
+ MediaSelector,
49
+ MediaSelectorMode,
50
+ } from "../media-selector/MediaSelector";
51
+ import { getComponentCommands } from "../commands/componentCommands";
52
+
53
+ import {
54
+ getLanguagesAndVersions,
55
+ getWorkbox,
56
+ } from "../services/contentService";
57
+ import ConfirmationDialog, {
58
+ ConfirmationDialogHandle,
59
+ ConfirmationProps,
60
+ } from "../ConfirmationDialog";
61
+
62
+ import MainLayout from "../MainLayout";
63
+ import { getItemDescriptor, useEventListenerExt } from "../utils";
64
+
65
+ import { EditContextMenu, EditContextMenuRef } from "../ContextMenu";
66
+
67
+ import { FieldEditorPopup, FieldEditorPopupRef } from "../FieldEditorPopup";
68
+
69
+ import { Command, CommandData } from "../commands/commands";
70
+ import { AiPopup, AiPopupRef } from "../ai/AiPopup";
71
+
72
+ import { ComponentDetails } from "../services/componentDesignerService";
73
+ import {
74
+ EditFieldOperation,
75
+ EditOperation,
76
+ EditSession,
77
+ FieldDescriptor,
78
+ HistoryEntry,
79
+ InsertOption,
80
+ LanguageVersions,
81
+ LinkComponentOperation,
82
+ MoveComponentOperation,
83
+ ValidationResult,
84
+ WorkboxItem,
85
+ Comment,
86
+ SuggestedEdit,
87
+ UserInfo,
88
+ } from "../../types";
89
+
90
+ import { post } from "../services/serviceHelper";
91
+ import { SidebarView } from "../sidebar/SidebarView";
92
+ import { PageViewerFrame } from "../page-viewer/PageViewerFrame";
93
+ import { ClientFieldButton } from "../../config/types";
94
+
95
+ import {
96
+ Component,
97
+ FieldButton,
98
+ ItemDescriptor,
99
+ FullItem,
100
+ Version,
101
+ Timings,
102
+ Field,
103
+ } from "../pageModel";
104
+
105
+ import { useItemsRepository } from "./itemsRepository";
106
+
107
+ import { Spinner } from "../ui/Spinner";
108
+ import { cleanId } from "../utils/id-helper";
109
+
110
+ import { useDebouncedCallback } from "use-debounce";
111
+
112
+ import { Tour } from "../../tour/Tour";
113
+ import { usePageViewContext } from "../page-viewer/pageViewContext";
114
+
115
+ import { getComments } from "../services/reviewsService";
116
+ import { AiTerminalOptions } from "../ai/AiTerminal";
117
+ import { useReviews } from "../reviews/useReviews";
118
+ import uuid from "react-uuid";
119
+ import { flushSync } from "react-dom";
120
+ import { getSuggestedEdits } from "../services/suggestedEditsService";
121
+ import { usePageWizard } from "../../page-wizard/usePageWizard";
122
+ import { requestQuota } from "../services/aiService";
123
+
124
+ export type FieldAction = {
125
+ field: FieldDescriptor;
126
+ message?: string;
127
+ state: "running" | "success" | "error";
128
+ label?: string;
129
+ };
130
+
131
+ export type WindowSize = { width: number; height: number };
132
+ export type InsertingState = {
133
+ positionElement: Element;
134
+ positionAnchor: "left" | "right" | "top" | "bottom";
135
+ };
136
+ export type QuotaUsage = {
137
+ totalTokens: number;
138
+ totalImages: number;
139
+ dailyTokens: number;
140
+ dailyImages: number;
141
+ };
142
+ export type QuotaLimits = {
143
+ totalTokens: number;
144
+ dailyTokens: number;
145
+ monthlyTokens: number;
146
+ totalImages: number;
147
+ dailyImages: number;
148
+ monthlyImages: number;
149
+ };
150
+ export type QuotaInfo = { usage: QuotaUsage; limits: QuotaLimits };
151
+
152
+ export type WebSocketMessage = {
153
+ id: string;
154
+ timestamp: string;
155
+ type: string;
156
+ payload: any;
157
+ rawMessage: string;
158
+ };
159
+
160
+ export function EditorClient({
161
+ configuration,
162
+ className,
163
+ item: loadItemDescriptor,
164
+ sessionId,
165
+ userInfo,
166
+ }: {
167
+ configuration: EditorConfiguration;
168
+ className?: string;
169
+ item?: ItemDescriptor;
170
+ sessionId: string;
171
+ userInfo: UserInfo;
172
+ }) {
173
+ const router = useRouter();
174
+
175
+ const pathname = usePathname();
176
+ const searchParams = useSearchParams();
177
+ const [selection, setSelection] = useState<string[]>([]);
178
+ const [selectedForInsertion, setSelectedForInsertion] = useState<string>("");
179
+
180
+ const [refreshCompletedFlag, setRefreshCompletedFlag] = useState(false);
181
+
182
+ const [isRefreshing, setIsRefreshing] = useState(false);
183
+ const [dragObject, setDragObject] = useState<DragObject>();
184
+ const [mediaResolver, setMediaResolver] = useState<(value: string) => void>();
185
+ const [mediaSelectorVisible, setMediaSelectorVisible] = useState(false);
186
+ const [mediaSelectorMode, setMediaSelectorMode] = useState<
187
+ "images" | "video"
188
+ >("images");
189
+ const [selectedMediaIdPath, setSelectedMediaIdPath] = useState<string>("");
190
+ const [scrollIntoView, setScrollIntoView] = useState<string>();
191
+
192
+ const confirmationDialogRef = useRef<ConfirmationDialogHandle>(null);
193
+ const contextMenuRef = useRef<EditContextMenuRef>(null);
194
+ const editContextRef = useRef<EditContextType>(undefined);
195
+
196
+ const [currentOverlay, setCurrentOverlay] = useState<string>();
197
+ const [contentEditorItem, setContentEditorItem] = useState<FullItem>();
198
+
199
+ const [focusedField, setFocusedField] = useState<FieldDescriptor>();
200
+ const [selectedRange, setSelectedRange] = useState<SelectionRange>();
201
+
202
+ const [validating, setValidating] = useState(false);
203
+
204
+ const [inserting, setInserting] = useState<InsertingState>();
205
+
206
+ const [showFullscreenHint, setShowFullscreenHint] = useState(false);
207
+ // const [showPublishDialog, setShowPublishDialog] = useState(false);
208
+ const [activeFieldActions, setActiveFieldActions] = useState<FieldAction[]>(
209
+ [],
210
+ );
211
+ const [renderedFields, setRenderedFields] = useState<FieldDescriptor[]>([]);
212
+
213
+ const aiPopupRef = React.useRef<AiPopupRef>(null);
214
+ const fieldEditorPopupRef = React.useRef<FieldEditorPopupRef>(null);
215
+
216
+ const [validationResult, setValidationResult] = useState<
217
+ ValidationResult[] | undefined
218
+ >();
219
+
220
+ const [editHistory, setEditHistory] = useState<EditOperation[]>([]);
221
+
222
+ const [lastEditedFields, setLastEditedFields] = useState<EditedField[]>([]);
223
+
224
+ const [activeSessions, setActiveSessions] = useState<EditSession[]>([]);
225
+
226
+ if (typeof window !== "undefined")
227
+ sessionStorage?.setItem("sessionId", sessionId);
228
+
229
+ const [viewName, setViewName] = useState<string>(
230
+ // default from query string
231
+ searchParams.get("view") ??
232
+ configuration.editor.views[0]?.name ??
233
+ "splash-screen",
234
+ );
235
+
236
+ const [compareMode, setCompareMode] = useState(false);
237
+ const [compareTo, setCompareTo] = useState<ItemDescriptor>();
238
+
239
+ const [componentDesignerComponent, setComponentDesignerComponent] =
240
+ useState<ComponentDetails>();
241
+ const [componentDesignerRendering, setComponentDesignerRendering] =
242
+ useState<FullItem>();
243
+
244
+ const [insertMode, setInsertMode] = useState(false);
245
+
246
+ const [ignoreBlur, setIgnoreBlur] = useState(false);
247
+
248
+ const [currentItemDescriptor, setCurrentItemDescriptor] =
249
+ useState<ItemDescriptor>();
250
+ const currentItemDescriptorRef = useRef<ItemDescriptor>(undefined);
251
+ useEffect(() => {
252
+ currentItemDescriptorRef.current = currentItemDescriptor;
253
+ }, [currentItemDescriptor]);
254
+
255
+ const currentItemRef = useRef<FullItem>(undefined);
256
+ useEffect(() => {
257
+ currentItemRef.current = contentEditorItem;
258
+ }, [contentEditorItem]);
259
+
260
+ const [inlineEditingFieldElement, setInlineEditingFieldElement] =
261
+ useState<HTMLElement>();
262
+
263
+ const [lockedField, setLockedField] = useState<FieldDescriptor>();
264
+
265
+ const [itemLanguages, setItemLanguages] = useState<LanguageVersions[]>([]);
266
+ const [itemVersions, setItemVersions] = useState<Version[]>([]);
267
+ const [modifiedFields, setModifiedFields] = useState<ModifiedField[]>([]);
268
+ const [comments, setComments] = useState<Comment[]>([]);
269
+ const [suggestedEdits, setSuggestedEdits] = useState<SuggestedEdit[]>([]);
270
+ const [showSuggestedEdits, setShowSuggestedEdits] = useState(false);
271
+ const [showSuggestedEditsDiff, setShowSuggestedEditsDiff] = useState(false);
272
+ const [showComments, setShowComments] = useState<boolean>(() => {
273
+ const savedShowComments =
274
+ typeof window !== "undefined"
275
+ ? localStorage.getItem("editor.showComments")
276
+ : null;
277
+ return savedShowComments ? JSON.parse(savedShowComments) : true;
278
+ });
279
+
280
+ useEffect(() => {
281
+ if (typeof window !== "undefined") {
282
+ localStorage.setItem("editor.showComments", JSON.stringify(showComments));
283
+ }
284
+ }, [showComments]);
285
+
286
+ const [selectedComment, setSelectedComment] = useState<Comment>();
287
+
288
+ const [browseHistory, setBrowseHistory] = useState<HistoryEntry[]>(() => {
289
+ const savedHistory =
290
+ typeof window !== "undefined"
291
+ ? localStorage.getItem("editor.browseHistory")
292
+ : null;
293
+ return savedHistory ? JSON.parse(savedHistory) : [];
294
+ });
295
+
296
+ const [centerPanelView, setCenterPanelView] = useState<ReactNode>();
297
+ const [timings, setTimings] = useState<Timings>({});
298
+
299
+ const [revision, setRevision] = useState<string>();
300
+
301
+ const [workboxItems, setWorkboxItems] = useState<WorkboxItem[]>([]);
302
+ const [isTourActive, setIsTourActive] = useState(false);
303
+
304
+ const [mode, setMode] = useState<EditorMode>("edit");
305
+
306
+ const [statusMessage, setStatusMessage] = useState<React.ReactNode>("");
307
+ const [focusFieldComponentId, setFocusFieldComponentId] = useState<string>();
308
+
309
+ const [enableCompletions, setEnableCompletions] = useState(false);
310
+ const [quotaInfo, setQuotaInfo] = useState<QuotaInfo | null>(null);
311
+ const pageWizard = usePageWizard();
312
+
313
+ const [webSocketMessages, setWebSocketMessages] = useState<
314
+ WebSocketMessage[]
315
+ >([]);
316
+
317
+ useEffect(() => {
318
+ const queryMode = searchParams.get("mode");
319
+ if (queryMode) setMode(queryMode as EditorMode);
320
+ }, [searchParams]);
321
+
322
+ useEffect(() => {
323
+ if (mode === "suggestions") {
324
+ setViewName("reviews");
325
+ }
326
+ }, [mode]);
327
+
328
+ useEffect(() => {
329
+ if (
330
+ focusedField &&
331
+ selection.length > 0 &&
332
+ selection[0] !== focusedField.item.id
333
+ ) {
334
+ setFocusedField(undefined);
335
+ }
336
+ }, [selection]);
337
+
338
+ const itemsRepository = useItemsRepository(
339
+ setModifiedFields,
340
+ setLastEditedFields,
341
+ );
342
+
343
+ const pageViewContext = usePageViewContext({
344
+ pageItemDescriptor: currentItemDescriptor,
345
+ itemsRepository,
346
+ configuration,
347
+ });
348
+
349
+ const socketMessageListeners = useRef<Set<(data: any) => void>>(new Set());
350
+
351
+ const addSocketMessageListener = (
352
+ callback: (message: { type: string; payload: any }) => void,
353
+ ) => {
354
+ socketMessageListeners.current.add(callback);
355
+ return () => socketMessageListeners.current.delete(callback);
356
+ };
357
+
358
+ const reviews = useReviews({
359
+ currentItemDescriptor: contentEditorItem?.descriptor,
360
+ addSocketMessageListener: addSocketMessageListener,
361
+ });
362
+
363
+ const validate = useDebouncedCallback(async (items: ItemDescriptor[]) => {
364
+ setValidating(true);
365
+ const result = await validateItems(items, sessionId);
366
+ if (result.type === "success") setValidationResult(await result.data);
367
+ setValidating(false);
368
+ }, 1000);
369
+
370
+ useEffect(() => {
371
+ setSelectedForInsertion("");
372
+ }, [selection]);
373
+
374
+ useEffect(() => {
375
+ if (focusedField?.fieldId !== selectedRange?.fieldId) {
376
+ setSelectedRange(undefined);
377
+ }
378
+ }, [focusedField]);
379
+
380
+ const currentView =
381
+ configuration.editor.views.find((x) => x.name === viewName) ??
382
+ configuration.editor.views[0];
383
+
384
+ useEffect(() => {
385
+ if (currentView?.defaultCenterPanelView)
386
+ setCenterPanelView(currentView.defaultCenterPanelView);
387
+ }, [currentView]);
388
+
389
+ const sendClientInfo = async () => {
390
+ const socket = (globalThis as any).editorSocket;
391
+
392
+ const clientInfoMessage = {
393
+ type: "client-info",
394
+ sessionId: sessionId,
395
+ url: window.location.href,
396
+ userAgent: navigator.userAgent,
397
+ item: currentItemRef.current
398
+ ? {
399
+ ...currentItemRef.current.descriptor,
400
+ name: currentItemRef.current.name,
401
+ }
402
+ : null,
403
+ };
404
+
405
+ if (socket.readyState !== WebSocket.OPEN) {
406
+ const url = "/alpaca/editor/client";
407
+ await post(url, clientInfoMessage);
408
+ } else {
409
+ socket.send(JSON.stringify(clientInfoMessage));
410
+ }
411
+ };
412
+
413
+ const startTour = () => {
414
+ setIsTourActive(true);
415
+ };
416
+
417
+ const messageHandler = useCallback(
418
+ async (event: any) => {
419
+ if (!event.data.startsWith("{")) return;
420
+ const message = JSON.parse(event.data);
421
+
422
+ // Track all WebSocket messages for debugging/monitoring
423
+ try {
424
+ const webSocketMessage: WebSocketMessage = {
425
+ id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
426
+ timestamp: new Date().toISOString(),
427
+ type: message.type || "unknown",
428
+ payload: message.payload,
429
+ rawMessage: JSON.stringify(message, null, 2),
430
+ };
431
+
432
+ setWebSocketMessages((prev) => {
433
+ const updated = [webSocketMessage, ...prev];
434
+ // Keep only the latest 1000 messages
435
+ return updated.slice(0, 1000);
436
+ });
437
+ } catch (error) {
438
+ console.error("Error tracking WebSocket message:", error);
439
+ }
440
+
441
+ if (message.type === "active-sessions") {
442
+ setActiveSessions(() => {
443
+ return message.payload;
444
+ });
445
+ }
446
+
447
+ if (message.type === "item-deleted") {
448
+ itemsRepository.onItemsDeleted([
449
+ { item: message.payload.item, parentId: message.payload.parentId },
450
+ ]);
451
+ if (message.payload.item.id === currentItemDescriptor?.id) {
452
+ console.log("Load", message.payload.parentId);
453
+ loadItem({
454
+ id: message.payload.parentId,
455
+ language: currentItemDescriptor?.language ?? "en",
456
+ version: 0,
457
+ });
458
+ }
459
+ }
460
+
461
+ if (message.type === "item-changed") {
462
+ await itemsRepository.refreshItems([message.payload.item]);
463
+ if (message.payload.item.id === currentItemDescriptor?.id)
464
+ loadItemVersions();
465
+ }
466
+
467
+ if (message.type === "item-version-added") {
468
+ if (currentItemDescriptorRef.current) {
469
+ if (currentItemDescriptorRef.current.id === message.payload.item.id)
470
+ await loadItemVersions();
471
+ setCurrentItemDescriptor({ ...currentItemDescriptorRef.current });
472
+ }
473
+ }
474
+
475
+ if (message.type === "comment-updated") {
476
+ setComments((x) => {
477
+ const newComments = [...x];
478
+ const index = newComments.findIndex(
479
+ (c) => c.id === message.payload.comment.id,
480
+ );
481
+ if (index !== -1) newComments[index] = message.payload.comment;
482
+ else newComments.push(message.payload.comment);
483
+ return newComments;
484
+ });
485
+ }
486
+
487
+ if (message.type === "comment-deleted") {
488
+ setComments((x) => {
489
+ return x.filter((c) => c.id !== message.payload.commentId);
490
+ });
491
+ }
492
+
493
+ if (message.type === "suggested-edit-updated") {
494
+ setSuggestedEdits((x) => {
495
+ const index = x.findIndex(
496
+ (s) => s.id === message.payload.suggestedEdit.id,
497
+ );
498
+ if (index !== -1) x[index] = message.payload.suggestedEdit;
499
+ else x.push(message.payload.suggestedEdit);
500
+ return x;
501
+ });
502
+ }
503
+
504
+ if (message.type === "suggested-edit-deleted") {
505
+ setSuggestedEdits((x) => {
506
+ return x.filter((s) => s.id !== message.payload.id);
507
+ });
508
+ }
509
+
510
+ if (message.type === "executing-field-action") {
511
+ setActiveFieldActions((x) => {
512
+ const payload = message.payload;
513
+ const fieldId = payload.fieldId;
514
+ const item = payload.item;
515
+ const status = payload.status;
516
+ const msg = payload.message;
517
+ const label = payload.label;
518
+
519
+ // Map backend status to FieldAction state
520
+ let state: "running" | "success" | "error";
521
+ switch (status?.toLowerCase()) {
522
+ case "completed":
523
+ case "success":
524
+ state = "success";
525
+ break;
526
+ case "failed":
527
+ case "error":
528
+ state = "error";
529
+ break;
530
+ default:
531
+ state = "running";
532
+ break;
533
+ }
534
+
535
+ // Check if action already exists
536
+ const existingActionIndex = x.findIndex(
537
+ (action) =>
538
+ action.field.fieldId === fieldId &&
539
+ action.field.item.id === item.id &&
540
+ action.field.item.language === item.language &&
541
+ action.field.item.version === item.version,
542
+ );
543
+
544
+ if (existingActionIndex !== -1) {
545
+ // Update existing action
546
+ const newActions = [...x];
547
+ newActions[existingActionIndex]!.state = state;
548
+ newActions[existingActionIndex]!.message = msg;
549
+ return newActions;
550
+ } else {
551
+ // Insert new action
552
+ const fieldDescriptor: FieldDescriptor = {
553
+ fieldId: fieldId,
554
+ item: item,
555
+ };
556
+
557
+ const newAction: FieldAction = {
558
+ field: fieldDescriptor,
559
+ state,
560
+ message: msg,
561
+ label: label,
562
+ };
563
+ // console.log(newAction);
564
+ return [...x, newAction];
565
+ }
566
+ });
567
+ }
568
+
569
+ if (message.type === "update-quota") {
570
+ setQuotaInfo(message.payload);
571
+ }
572
+
573
+ if (message.type === "edit-operation") {
574
+ const op = message.payload as EditOperation;
575
+
576
+ if (op.type === "edit-field") {
577
+ const editFieldOperation = op as EditFieldOperation;
578
+
579
+ const field = await itemsRepository.getField({
580
+ item: {
581
+ ...editFieldOperation.mainItem,
582
+ id: editFieldOperation.itemId,
583
+ },
584
+ fieldId: editFieldOperation.fieldId,
585
+ });
586
+
587
+ if (
588
+ !field ||
589
+ (field.type !== "single-line text" &&
590
+ field.type !== "multi-line text" &&
591
+ field.type !== "rich text")
592
+ ) {
593
+ itemsRepository.refreshItems([
594
+ {
595
+ ...editFieldOperation.mainItem,
596
+ id: editFieldOperation.itemId,
597
+ },
598
+ ]);
599
+ requestRefresh("immediate");
600
+ }
601
+ //TODO: field value changes that require rerender
602
+ else
603
+ itemsRepository.updateFieldValue(
604
+ {
605
+ fieldId: editFieldOperation.fieldId,
606
+ item: {
607
+ ...editFieldOperation.mainItem,
608
+ id: editFieldOperation.itemId,
609
+ },
610
+ },
611
+ editFieldOperation.user ?? { name: "unknown", ai: false },
612
+ false,
613
+ editFieldOperation.undone
614
+ ? editFieldOperation.oldValue
615
+ : editFieldOperation.value,
616
+ );
617
+ } else {
618
+ requestRefresh("immediate");
619
+ }
620
+
621
+ if (
622
+ op.mainItem &&
623
+ op.mainItem.id === currentItemRef.current?.descriptor.id &&
624
+ op.mainItem.language ===
625
+ currentItemRef.current?.descriptor.language &&
626
+ op.mainItem.version === currentItemRef.current?.descriptor.version
627
+ ) {
628
+ loadHistory(op.mainItem);
629
+ }
630
+ }
631
+
632
+ socketMessageListeners.current.forEach((listener) => listener(message));
633
+ },
634
+ [currentItemDescriptorRef, setLastEditedFields],
635
+ );
636
+
637
+ const user = activeSessions.find((x) => x.sessionId === sessionId)?.user;
638
+
639
+ useEffect(() => {
640
+ if (typeof window === "undefined") return;
641
+ const keepAliveUrl = "/alpaca/editor/keepalive";
642
+ const interval = setInterval(
643
+ () => {
644
+ fetch(keepAliveUrl + "?ts=" + Date.now()).catch((error) =>
645
+ console.error("Keep Alive error:", error),
646
+ );
647
+ },
648
+ 5 * 60 * 1000,
649
+ );
650
+
651
+ const handleMessage = (event: MessageEvent) => {
652
+ if (event.data.type === "componentsSelected") {
653
+ setSelection(event.data.componentIds);
654
+ }
655
+ };
656
+
657
+ window.addEventListener("message", handleMessage);
658
+
659
+ return () => {
660
+ window.removeEventListener("message", handleMessage);
661
+ clearInterval(interval);
662
+ };
663
+ }, []);
664
+
665
+ // Custom hook for responsive design
666
+ const useMediaQuery = (query: string) => {
667
+ const [matches, setMatches] = useState(false);
668
+
669
+ useEffect(() => {
670
+ const media = window.matchMedia(query);
671
+ const updateMatch = () => setMatches(media.matches);
672
+
673
+ // Set initial value
674
+ updateMatch();
675
+
676
+ // Listen for changes
677
+ media.addEventListener("change", updateMatch);
678
+
679
+ // Cleanup
680
+ return () => {
681
+ media.removeEventListener("change", updateMatch);
682
+ };
683
+ }, [query]);
684
+
685
+ return matches;
686
+ };
687
+
688
+ // Detect mobile screens (max-width: 768px)
689
+ const isMobile = useMediaQuery("(max-width: 768px)");
690
+
691
+ useEffect(() => {
692
+ const tour = configuration.activeTour;
693
+ const key =
694
+ tour === "default" ? "editor.tourShown" : "editor.tourShown." + tour;
695
+ const tourShown = localStorage.getItem(key);
696
+
697
+ if (user?.isLimitedPreviewUser && !tourShown) {
698
+ startTour();
699
+ localStorage.setItem(key, "true");
700
+ }
701
+ }, [user]);
702
+
703
+ useEffect(() => {
704
+ let reconnectTimeout: NodeJS.Timeout | null = null;
705
+ let reconnectAttempts = 0;
706
+
707
+ const connectWebSocket = () => {
708
+ let socket: WebSocket = (globalThis as any).editorSocket;
709
+
710
+ if (
711
+ socket &&
712
+ (socket.readyState === WebSocket.OPEN ||
713
+ socket.readyState === WebSocket.CONNECTING)
714
+ )
715
+ return;
716
+
717
+ socket = connectSocket(sessionId);
718
+
719
+ // Connection opened
720
+ socket.addEventListener("open", () => {
721
+ console.log("Connected!");
722
+ reconnectAttempts = 0; // Reset attempts on successful connection
723
+ sendClientInfo();
724
+ requestQuota();
725
+ //TODO: Load clients
726
+ });
727
+
728
+ // Listen for messages
729
+ socket.addEventListener("message", messageHandler);
730
+
731
+ // Handle connection close
732
+ socket.addEventListener("close", (event) => {
733
+ console.log("WebSocket connection closed:", event.code, event.reason);
734
+
735
+ // Only attempt to reconnect if it wasn't a clean close
736
+ if (event.code !== 1000) {
737
+ // Start with 1 second, increase exponentially up to 30 seconds
738
+ const delay = Math.min(
739
+ 1000 * Math.pow(2, Math.min(reconnectAttempts, 5)),
740
+ 30000,
741
+ );
742
+ console.log(
743
+ `Attempting to reconnect in ${delay}ms... (attempt ${reconnectAttempts + 1})`,
744
+ );
745
+
746
+ reconnectTimeout = setTimeout(() => {
747
+ reconnectAttempts++;
748
+ connectWebSocket();
749
+ }, delay);
750
+ }
751
+ });
752
+
753
+ // Handle connection errors
754
+ socket.addEventListener("error", (error) => {
755
+ console.error("WebSocket error:", error);
756
+ });
757
+
758
+ (globalThis as any).editorSocket = socket;
759
+ };
760
+
761
+ connectWebSocket();
762
+
763
+ // Cleanup function
764
+ return () => {
765
+ if (reconnectTimeout) {
766
+ clearTimeout(reconnectTimeout);
767
+ }
768
+ };
769
+ }, []);
770
+
771
+ useEffect(() => {
772
+ const itemid = searchParams.get("itemid");
773
+
774
+ if (searchParams.has("view")) {
775
+ setViewName(searchParams.get("view")!);
776
+ } else if (!itemid) {
777
+ setViewName("splash-screen");
778
+ }
779
+
780
+ if (searchParams.has("compare"))
781
+ setCompareMode(searchParams.get("compare") === "true");
782
+ const itemId = cleanId(loadItemDescriptor?.id ?? itemid ?? undefined);
783
+ const language = loadItemDescriptor?.language ?? searchParams.get("lang");
784
+ const version =
785
+ loadItemDescriptor?.version ??
786
+ (searchParams.has("version")
787
+ ? parseInt(searchParams.get("version")!)
788
+ : 0);
789
+
790
+ // if (itemid && viewName === "splash-screen") {
791
+ // setViewName("page-editor");
792
+ // }
793
+
794
+ if (!itemId || !language) return;
795
+
796
+ if (
797
+ currentItemDescriptor?.id === itemId &&
798
+ currentItemDescriptor?.language === language &&
799
+ (!version || currentItemDescriptor?.version === version)
800
+ ) {
801
+ return;
802
+ }
803
+
804
+ loadItem({ id: itemId, language, version });
805
+ }, [searchParams, loadItemDescriptor]);
806
+
807
+ useEffect(() => {
808
+ if (selection.length) setSelectedForInsertion("");
809
+
810
+ // Does the current focused field belong to the current item?
811
+ const currentItem =
812
+ selection.length > 0 ? selection[0] : pageViewContext.page?.item.id;
813
+ if (currentItem && focusedField?.item.id !== currentItem) {
814
+ setFocusedField(undefined);
815
+ }
816
+ }, [selection]);
817
+
818
+ const addToEditHistory = useCallback(
819
+ (currentEditOperation: EditOperation) => {
820
+ setEditHistory((history) => {
821
+ // Check if the operation was updated or needs to be added
822
+ const exists = history.some(
823
+ (item) => item.id === currentEditOperation.id,
824
+ );
825
+ return exists ? history : [currentEditOperation, ...history];
826
+ });
827
+ },
828
+ [],
829
+ );
830
+
831
+ useEffect(() => {
832
+ setRefreshCompletedFlag(!refreshCompletedFlag);
833
+ setInserting(undefined);
834
+ }, [currentItemDescriptor, pageViewContext.page]);
835
+
836
+ useEffect(() => {
837
+ sendClientInfo();
838
+ }, [currentItemDescriptor]);
839
+
840
+ const loadComments = useCallback(async () => {
841
+ if (!currentItemDescriptor) return;
842
+ const result = await getComments(
843
+ currentItemDescriptor.id,
844
+ currentItemDescriptor.language,
845
+ currentItemDescriptor.version,
846
+ );
847
+ if (handleErrorResult(result, ui, state)) return;
848
+ setComments((x) => {
849
+ const loadedComments = result.data as Comment[];
850
+ const newComments = x.filter(
851
+ (c) =>
852
+ c.isNew &&
853
+ c.mainItemId === currentItemDescriptor.id &&
854
+ c.language === currentItemDescriptor.language &&
855
+ c.version === currentItemDescriptor.version,
856
+ );
857
+ const allComments = [...loadedComments, ...newComments];
858
+ allComments.sort((a, b) => a.position - b.position);
859
+ return allComments;
860
+ });
861
+ }, [currentItemDescriptor]);
862
+
863
+ // Assuming currentItemDescriptor, ui, state, handleErrorResult, and setSuggestedEdits
864
+ // are available in your component context.
865
+ const loadSuggestedEdits = useCallback(async () => {
866
+ if (!contentEditorItem) return;
867
+
868
+ const result = await getSuggestedEdits(
869
+ contentEditorItem.descriptor.id,
870
+ contentEditorItem.descriptor.language,
871
+ contentEditorItem.descriptor.version,
872
+ );
873
+
874
+ if (handleErrorResult(result, ui, state)) return;
875
+
876
+ setSuggestedEdits(result.data || []);
877
+ }, [contentEditorItem]);
878
+
879
+ const page = pageViewContext.page;
880
+
881
+ useEffect(() => {
882
+ const isLoading =
883
+ !!contentEditorItem &&
884
+ contentEditorItem.hasLayout &&
885
+ (!page || (page?.editRevision ?? "") != (revision ?? ""));
886
+ setIsRefreshing(isLoading && viewName === "page-editor");
887
+ if (!isLoading) setInserting(undefined);
888
+ }, [page, viewName, revision]);
889
+
890
+ useEffect(() => {
891
+ if (searchParams.get("fullscreen")) {
892
+ pageViewContext.setFullscreen(true);
893
+ }
894
+ const handleMessage = (event: MessageEvent) => {
895
+ if (event.data.action === "refresh") {
896
+ requestRefresh("immediate");
897
+ }
898
+ };
899
+
900
+ window.addEventListener("message", handleMessage);
901
+
902
+ return () => {
903
+ window.removeEventListener("message", handleMessage);
904
+ };
905
+ }, []);
906
+
907
+ const loadHistory = useDebouncedCallback(async (item: ItemDescriptor) => {
908
+ const result = await getEditHistory(item);
909
+ if (handleErrorResult(result, ui, state)) return;
910
+ setEditHistory(result.data || []);
911
+ }, 200);
912
+
913
+ const refreshHistory = useCallback(async (item: ItemDescriptor) => {
914
+ const result = await getEditHistory(item);
915
+ if (handleErrorResult(result, ui, state)) return;
916
+ setEditHistory(result.data || []);
917
+ }, []);
918
+
919
+ const requestRefresh = useCallback(
920
+ (mode?: "immediate" | "delayed" | "waitForQuietPeriod") => {
921
+ const refreshTimer = (globalThis as any).editorRefreshTimer;
922
+ const doRefresh = () => {
923
+ //console.log("Refreshing");
924
+ //const url = new URL(window.location.href);
925
+ //url.searchParams.set("edit_rev", uuid());
926
+ const newRevision = uuid();
927
+ setRevision(newRevision);
928
+ console.log("doRefresh", newRevision);
929
+ //router.replace(url.toString(), { scroll: false });
930
+ (globalThis as any).editorRefreshTimer = undefined;
931
+ };
932
+ if (mode === "immediate") {
933
+ // console.error(
934
+ // "Immediate refresh requested. Stack trace:",
935
+ // new Error().stack
936
+ // );
937
+ console.log("Immediate refresh requested");
938
+ doRefresh();
939
+ return;
940
+ }
941
+ //console.log("request refresh with mode: " + mode, "timer: ", refreshTimer);
942
+ if (mode === "waitForQuietPeriod" && refreshTimer) {
943
+ clearTimeout(refreshTimer);
944
+ }
945
+ if (!refreshTimer || mode === "waitForQuietPeriod") {
946
+ (globalThis as any).editorRefreshTimer = setTimeout(
947
+ () => {
948
+ doRefresh();
949
+ },
950
+ mode === "waitForQuietPeriod" ? 1200 : 700,
951
+ );
952
+ }
953
+ },
954
+ [contentEditorItem, router],
955
+ );
956
+
957
+ useEffect(() => {
958
+ if (!currentItemDescriptor) return;
959
+ loadComments();
960
+ loadSuggestedEdits();
961
+ }, [currentItemDescriptor]);
962
+
963
+ // Automatically focus the first rendered field when a new component is added
964
+ useEffect(() => {
965
+ async function setFocus() {
966
+ if (!focusFieldComponentId) return;
967
+
968
+ const component = findComponent(
969
+ focusFieldComponentId,
970
+ page?.rootComponent?.placeholders ?? [],
971
+ );
972
+
973
+ if (component?.datasourceItem) {
974
+ const item = await itemsRepository.getItem(component.datasourceItem);
975
+ if (!item) return;
976
+ const field = item.fields.find(
977
+ (f) =>
978
+ component.datasourceItem?.renderedFieldIds?.includes(f.id) &&
979
+ (f.type === "single-line text" ||
980
+ f.type === "multi-line text" ||
981
+ f.type === "rich text"),
982
+ );
983
+ if (field) {
984
+ setFocusedField(field.descriptor);
985
+ setFocusFieldComponentId(undefined);
986
+ }
987
+ }
988
+ }
989
+ if (mode === "edit") setFocus();
990
+ }, [page]);
991
+
992
+ useEffect(() => {
993
+ if (!currentItemDescriptor) return;
994
+
995
+ const current = new URLSearchParams(Array.from(searchParams.entries()));
996
+ if (current.get("itemid") !== currentItemDescriptor?.id) {
997
+ current.set("itemid", currentItemDescriptor.id);
998
+ }
999
+ if (current.get("lang") !== currentItemDescriptor?.language) {
1000
+ current.set("lang", currentItemDescriptor.language);
1001
+ }
1002
+ if (current.get("version") !== currentItemDescriptor?.version.toString()) {
1003
+ current.set("version", currentItemDescriptor.version.toString());
1004
+ }
1005
+ if (current.get("view") !== viewName) {
1006
+ current.set("view", viewName);
1007
+ }
1008
+ if (!compareMode) {
1009
+ current.delete("compare");
1010
+ current.delete("compareLanguage");
1011
+ current.delete("compareVersion");
1012
+ current.delete("compareDevice");
1013
+ } else {
1014
+ current.set("compare", "true");
1015
+ }
1016
+
1017
+ current.set("mode", mode);
1018
+
1019
+ const newUrl = `${pathname}?${current.toString()}`;
1020
+ router.push(newUrl, { scroll: false });
1021
+ }, [currentItemDescriptor, viewName, compareMode, mode]);
1022
+
1023
+ useEffect(() => {
1024
+ async function load() {
1025
+ if (!currentItemDescriptor) return;
1026
+ const item = await itemsRepository.getItem(currentItemDescriptor);
1027
+
1028
+ setContentEditorItem(item);
1029
+ if (!item) return;
1030
+ if (
1031
+ contentEditorItem?.descriptor.id === currentItemDescriptor.id &&
1032
+ contentEditorItem?.descriptor.language ===
1033
+ currentItemDescriptor.language &&
1034
+ contentEditorItem?.descriptor.version !== item.version
1035
+ ) {
1036
+ showInfoToast({
1037
+ summary: "New version!",
1038
+ details: "New version of item loaded",
1039
+ });
1040
+ }
1041
+ }
1042
+ load();
1043
+ }, [itemsRepository.revision]);
1044
+
1045
+ const addToBrowseHistory = useCallback(
1046
+ (item: FullItem) => {
1047
+ const historyEntry = {
1048
+ path: item.path || "unknown",
1049
+ name: item.name || "unknown",
1050
+ language: item.language,
1051
+ templateName: item.templateName,
1052
+ id: item.id,
1053
+ hasLayout: item.hasLayout,
1054
+ icon: item.icon,
1055
+ version: item.version,
1056
+ };
1057
+
1058
+ setBrowseHistory((history: HistoryEntry[]) => {
1059
+ const newItem = item;
1060
+
1061
+ if (!newItem) return history;
1062
+
1063
+ const newHistory = [
1064
+ historyEntry,
1065
+ ...history
1066
+ .filter(
1067
+ (x) => x.id !== newItem.id || x.language !== newItem.language,
1068
+ )
1069
+ .slice(0, 25),
1070
+ ];
1071
+
1072
+ localStorage.setItem(
1073
+ "editor.browseHistory",
1074
+ JSON.stringify(newHistory),
1075
+ );
1076
+
1077
+ return newHistory;
1078
+ });
1079
+ },
1080
+ [browseHistory, setBrowseHistory],
1081
+ );
1082
+
1083
+ const loadItem = useCallback(
1084
+ async (
1085
+ itemToLoad: ItemDescriptor | string,
1086
+ options?: { addToBrowseHistory?: boolean },
1087
+ ): Promise<FullItem | undefined> => {
1088
+ if (typeof itemToLoad === "string")
1089
+ itemToLoad = {
1090
+ id: itemToLoad,
1091
+ language: contentEditorItem?.language || "en",
1092
+ version: 0,
1093
+ };
1094
+
1095
+ console.log("load item: " + itemToLoad.id, itemToLoad.language);
1096
+ const item = await itemsRepository.getItem(itemToLoad);
1097
+
1098
+ if (!item) {
1099
+ //TODO: Show error
1100
+ console.log("item not found: ", itemToLoad.id, itemToLoad.language);
1101
+ return undefined;
1102
+ }
1103
+
1104
+ if (
1105
+ (!item.hasLayout && viewName === "page-editor") ||
1106
+ viewName == "splash-screen"
1107
+ ) {
1108
+ setViewName("content-editor");
1109
+ }
1110
+
1111
+ // ensure this is object has no additional properties
1112
+ itemToLoad = getItemDescriptor(itemToLoad);
1113
+
1114
+ // Set state for the item
1115
+ setCurrentItemDescriptor(itemToLoad);
1116
+ setContentEditorItem(item);
1117
+ setSelection([]);
1118
+
1119
+ // Directly update URL here
1120
+ const current = new URLSearchParams(Array.from(searchParams.entries()));
1121
+
1122
+ current.set("itemid", itemToLoad.id);
1123
+ current.set("lang", itemToLoad.language);
1124
+ current.set("version", itemToLoad.version.toString());
1125
+ current.set("view", viewName);
1126
+
1127
+ if (!compareMode) {
1128
+ current.delete("compare");
1129
+ current.delete("compareLanguage");
1130
+ current.delete("compareVersion");
1131
+ current.delete("compareDevice");
1132
+ } else {
1133
+ current.set("compare", "true");
1134
+ }
1135
+
1136
+ current.set("mode", mode);
1137
+
1138
+ const newUrl = `${pathname}?${current.toString()}`;
1139
+
1140
+ // Wait for the URL update to complete
1141
+ await router.push(newUrl, { scroll: false });
1142
+
1143
+ // Now that URL is updated, load history and add to browse history
1144
+ loadHistory(itemToLoad);
1145
+ if (
1146
+ options?.addToBrowseHistory ||
1147
+ options?.addToBrowseHistory === undefined
1148
+ ) {
1149
+ addToBrowseHistory(item);
1150
+ }
1151
+
1152
+ return item;
1153
+ },
1154
+ [
1155
+ itemsRepository,
1156
+ setContentEditorItem,
1157
+ searchParams,
1158
+ pathname,
1159
+ router,
1160
+ viewName,
1161
+ compareMode,
1162
+ ],
1163
+ );
1164
+
1165
+ useEffect(() => {
1166
+ if (pageViewContext.fullscreen && !searchParams.get("fullscreen"))
1167
+ setShowFullscreenHint(true);
1168
+ }, [pageViewContext.fullscreen]);
1169
+
1170
+ const executeCommand = useCallback(
1171
+ async <T extends CommandData>({
1172
+ command,
1173
+ event,
1174
+ data,
1175
+ }: {
1176
+ command: Command<T>;
1177
+ data?: any;
1178
+ event?: React.SyntheticEvent;
1179
+ }): Promise<any> => {
1180
+ if (!editContextRef.current) return;
1181
+ const context = {
1182
+ editContext: editContextRef.current,
1183
+ pathname,
1184
+ router,
1185
+ searchParams,
1186
+ openDialog: openDialog,
1187
+ event,
1188
+ data,
1189
+ };
1190
+ const result = await command.execute(context);
1191
+
1192
+ return result;
1193
+ },
1194
+ [editContextRef, pathname, router, searchParams],
1195
+ );
1196
+
1197
+ const handleKeyDownDebounced = useDebouncedCallback(
1198
+ async (event: KeyboardEvent) => {
1199
+ if (!event.key) return;
1200
+
1201
+ if (event.ctrlKey && event.key === "z") {
1202
+ await operations.undo();
1203
+ }
1204
+ if (event.ctrlKey && event.key === "y") {
1205
+ await operations.redo();
1206
+ }
1207
+ if (event.ctrlKey && event.key === "F11") {
1208
+ event.preventDefault();
1209
+ pageViewContext.setFullscreen(false);
1210
+ }
1211
+
1212
+ const command = configuration.commands.allItemCommands.find(
1213
+ (x) => x.keyBinding === event.key,
1214
+ );
1215
+
1216
+ if (command) {
1217
+ event.preventDefault();
1218
+ const contentEditorItem = editContextRef.current?.contentEditorItem;
1219
+ if (!contentEditorItem) return;
1220
+
1221
+ const items =
1222
+ editContextRef.current?.selection?.map((x) => ({
1223
+ id: x,
1224
+ language: contentEditorItem.language,
1225
+ version: 0,
1226
+ })) || [];
1227
+
1228
+ if (!items.length) items.push(contentEditorItem.descriptor);
1229
+
1230
+ if (items.length > 0) {
1231
+ const fullItems =
1232
+ await editContextRef.current?.itemsRepository.getItems(items);
1233
+ executeCommand({
1234
+ command,
1235
+ data: {
1236
+ items: fullItems,
1237
+ },
1238
+ });
1239
+ }
1240
+ }
1241
+ },
1242
+ 50,
1243
+ );
1244
+
1245
+ const handleKeyDown = useCallback(
1246
+ async (event: KeyboardEvent) => {
1247
+ if (event.key === "Insert") {
1248
+ event.preventDefault();
1249
+ event.stopPropagation();
1250
+ editContext.setInsertMode((x) => !x);
1251
+ }
1252
+
1253
+ if (event.ctrlKey && event.key === "s") {
1254
+ event.preventDefault();
1255
+ event.stopPropagation();
1256
+ return;
1257
+ }
1258
+
1259
+ const target = event.target as HTMLElement;
1260
+ const isTyping =
1261
+ target instanceof HTMLInputElement ||
1262
+ target instanceof HTMLTextAreaElement ||
1263
+ target.isContentEditable;
1264
+
1265
+ if (
1266
+ (event.ctrlKey && event.key === "z") ||
1267
+ (event.ctrlKey && event.key === "y")
1268
+ ) {
1269
+ if (!isTyping) {
1270
+ event.preventDefault();
1271
+ event.stopPropagation();
1272
+ handleKeyDownDebounced(event);
1273
+ }
1274
+ return;
1275
+ }
1276
+ handleKeyDownDebounced(event);
1277
+ },
1278
+ [
1279
+ configuration.commands.allItemCommands,
1280
+ executeCommand,
1281
+ editContextRef.current,
1282
+ ],
1283
+ );
1284
+
1285
+ if (typeof window !== "undefined")
1286
+ if (typeof window !== "undefined")
1287
+ useEventListenerExt("keydown", handleKeyDown, window, true);
1288
+
1289
+ if (typeof window !== "undefined")
1290
+ useEventListenerExt(
1291
+ "click",
1292
+ () => {
1293
+ contextMenuRef.current?.close({});
1294
+ },
1295
+ window,
1296
+ true,
1297
+ );
1298
+ const state = useMemo(
1299
+ () => ({
1300
+ page,
1301
+ configuration,
1302
+ selection,
1303
+ setSelection,
1304
+ loadItem,
1305
+ addToEditHistory,
1306
+ setLockedField,
1307
+ contentEditorItem,
1308
+ sessionId,
1309
+ requestRefresh,
1310
+ lockedField,
1311
+ itemsRepository,
1312
+ user,
1313
+ editHistory,
1314
+ refreshHistory,
1315
+ suggestedEdits,
1316
+ setSuggestedEdits,
1317
+ mode,
1318
+ setFocusFieldComponentId,
1319
+ }),
1320
+ [
1321
+ page,
1322
+ configuration,
1323
+ selection,
1324
+ setSelection,
1325
+ loadItem,
1326
+ addToEditHistory,
1327
+ setLockedField,
1328
+ contentEditorItem,
1329
+ sessionId,
1330
+ requestRefresh,
1331
+ lockedField,
1332
+ itemsRepository,
1333
+ user,
1334
+ editHistory,
1335
+ refreshHistory,
1336
+ suggestedEdits,
1337
+ setSuggestedEdits,
1338
+ mode,
1339
+ setFocusFieldComponentId,
1340
+ ],
1341
+ );
1342
+
1343
+ useEffect(() => {
1344
+ if (currentOverlay !== "ai") aiPopupRef.current?.close();
1345
+ if (currentOverlay !== "fields") fieldEditorPopupRef.current?.close();
1346
+ if (currentOverlay !== "context-menu") contextMenuRef.current?.close({});
1347
+ }, [currentOverlay]);
1348
+
1349
+ const toast = useRef<Toast | null>(null);
1350
+
1351
+ useEffect(() => {
1352
+ loadItemVersions();
1353
+ }, [currentItemDescriptor]);
1354
+
1355
+ const loadItemVersions = useCallback(async () => {
1356
+ if (!currentItemDescriptorRef.current) {
1357
+ setItemVersions([]);
1358
+ setItemLanguages([]);
1359
+ return;
1360
+ }
1361
+
1362
+ const result = await getLanguagesAndVersions(
1363
+ currentItemDescriptorRef.current,
1364
+ );
1365
+ if (!result?.data) {
1366
+ setItemVersions([]);
1367
+ setItemLanguages([]);
1368
+ showErrorToast({
1369
+ summary: "Error",
1370
+ details: "Failed to load item versions",
1371
+ });
1372
+ return;
1373
+ }
1374
+ const v = [...result.data.versions].reverse() || [];
1375
+
1376
+ setItemVersions(v);
1377
+ setItemLanguages(result.data.languages);
1378
+ }, [currentItemDescriptorRef.current, setItemVersions, setItemLanguages]);
1379
+
1380
+ const showErrorToast = useCallback(
1381
+ ({ summary, details }: { summary?: string; details?: string }) => {
1382
+ toast.current?.show({
1383
+ severity: "error",
1384
+ summary: summary || "Error",
1385
+ detail: details || "An error occurred",
1386
+ life: 3000,
1387
+ });
1388
+ },
1389
+ [],
1390
+ );
1391
+
1392
+ const showInfoToast = useCallback(
1393
+ ({ summary, details }: { summary?: string; details?: string }) => {
1394
+ toast.current?.show({
1395
+ severity: "info",
1396
+ summary: summary || "Info",
1397
+ detail: details || "Information",
1398
+ life: 3000,
1399
+ });
1400
+ },
1401
+ [],
1402
+ );
1403
+
1404
+ const ui = useMemo(
1405
+ () => ({
1406
+ showErrorToast,
1407
+ confirmationDialogRef,
1408
+ onOperationExecuted: (op: EditOperation) => {
1409
+ // Replace the operation in edit history with the executed operation
1410
+
1411
+ setEditHistory((prev) => {
1412
+ const existingOpIndex = prev.findIndex((x) => x.id === op.id);
1413
+ if (existingOpIndex >= 0) {
1414
+ prev[existingOpIndex] = op;
1415
+ }
1416
+ return prev;
1417
+ });
1418
+
1419
+ if (
1420
+ contentEditorItem?.id === op.mainItem?.id &&
1421
+ contentEditorItem?.language === op.mainItem?.language &&
1422
+ contentEditorItem?.version === op.mainItem?.version
1423
+ ) {
1424
+ setInsertMode(false);
1425
+ }
1426
+ },
1427
+ }),
1428
+ [showErrorToast, confirmationDialogRef, currentItemDescriptor],
1429
+ );
1430
+
1431
+ const selectMedia = useCallback(
1432
+ ({
1433
+ selectedIdPath,
1434
+ mode,
1435
+ }: {
1436
+ selectedIdPath: string;
1437
+ mode?: MediaSelectorMode;
1438
+ }) => {
1439
+ setSelectedMediaIdPath(selectedIdPath);
1440
+ setMediaSelectorVisible(true);
1441
+ if (mode) setMediaSelectorMode(mode);
1442
+ return new Promise<string>((resolve) => {
1443
+ setMediaResolver(() => resolve);
1444
+ });
1445
+ },
1446
+ [],
1447
+ );
1448
+
1449
+ const onMediaSelect = useCallback(
1450
+ (mediaUrl: string) => {
1451
+ mediaResolver?.(mediaUrl);
1452
+ setMediaSelectorVisible(false);
1453
+ setMediaResolver(undefined);
1454
+ },
1455
+ [mediaResolver],
1456
+ );
1457
+
1458
+ useEffect(() => {
1459
+ if (!workboxItems || workboxItems.length === 0) return;
1460
+ const itemsToValidate = workboxItems.map((x) => x.item);
1461
+ validate(itemsToValidate);
1462
+ }, [workboxItems]);
1463
+
1464
+ async function loadWorkbox(items: ItemDescriptor[]) {
1465
+ if (!items.length) {
1466
+ setWorkboxItems([]);
1467
+ return;
1468
+ }
1469
+
1470
+ const workbox = await getWorkbox(items);
1471
+ const workboxItems: WorkboxItem[] = workbox.data || [];
1472
+
1473
+ const sortedWorkboxItems = workboxItems.sort((a, b) => {
1474
+ if (a.isPublished === b.isPublished)
1475
+ return (
1476
+ (b.workflowCommands?.length || 0) - (a.workflowCommands?.length || 0)
1477
+ );
1478
+ return !a.isPublished || !a.isPublishable ? -1 : 1;
1479
+ });
1480
+
1481
+ setWorkboxItems(sortedWorkboxItems);
1482
+ }
1483
+
1484
+ const loadWorkboxDebounced = useDebouncedCallback(
1485
+ (items: ItemDescriptor[]) => loadWorkbox(items),
1486
+ 5000,
1487
+ );
1488
+
1489
+ useEffect(() => {
1490
+ const items: ItemDescriptor[] = [];
1491
+
1492
+ if (editContext.contentEditorItem) {
1493
+ items.push(editContext.contentEditorItem.descriptor);
1494
+ }
1495
+
1496
+ if (editContext.page) {
1497
+ collectAllItems(editContext.page.rootComponent, items);
1498
+ }
1499
+
1500
+ loadWorkboxDebounced(items.filter((x) => x));
1501
+ }, [page, contentEditorItem]);
1502
+
1503
+ function collectAllItems(component: Component, items: ItemDescriptor[]) {
1504
+ component.placeholders.forEach((x) => {
1505
+ x.components.forEach((y) => {
1506
+ if (y.isShared && y.datasourceItem) {
1507
+ items.push(y.datasourceItem);
1508
+ }
1509
+
1510
+ //TODO: Add picture fields
1511
+ // y.datasourceItem?.fields.forEach((z) => {
1512
+ // if (z.type === "picture") {
1513
+ // const picture = z.value as PictureValue;
1514
+ // if (picture.variants) {
1515
+ // picture.variants.forEach((v) => {
1516
+ // if (v.mediaId) {
1517
+ // items.push({
1518
+ // id: v.mediaId,
1519
+ // language: y.datasourceItem!.descriptor.language,
1520
+ // version: 0,
1521
+ // });
1522
+ // }
1523
+ // });
1524
+ // }
1525
+ // }
1526
+ // });
1527
+
1528
+ collectAllItems(y, items);
1529
+ });
1530
+ });
1531
+ }
1532
+
1533
+ const switchView = (
1534
+ viewName: string,
1535
+ options?: { skipConfirmation?: boolean },
1536
+ ) => {
1537
+ async function switchView() {
1538
+ if (currentView?.beforeClose && !options?.skipConfirmation) {
1539
+ const result = await currentView.beforeClose(editContext);
1540
+ if (!result) return;
1541
+ }
1542
+ if (typeof document.startViewTransition === "function") {
1543
+ document.startViewTransition(() => {
1544
+ flushSync(() => {
1545
+ setViewName(viewName);
1546
+ });
1547
+ });
1548
+ } else {
1549
+ setViewName(viewName);
1550
+ }
1551
+ }
1552
+ switchView();
1553
+ };
1554
+
1555
+ const [dialog, setDialog] = useState<ReactNode>(null);
1556
+
1557
+ const openDialog: OpenDialog = (Component, props) => {
1558
+ return new Promise((resolve) => {
1559
+ const handleClose = (result: any) => {
1560
+ setDialog(null);
1561
+ resolve(result);
1562
+ };
1563
+
1564
+ setDialog(<Component {...(props as any)} onClose={handleClose} />);
1565
+ });
1566
+ };
1567
+
1568
+ const operationsContext = getOperationsContext(state, ui);
1569
+
1570
+ const operations = operationsContext.ops;
1571
+
1572
+ useEffect(() => {
1573
+ if (mode === "suggestions") {
1574
+ setShowSuggestedEdits(true);
1575
+ } else {
1576
+ setShowSuggestedEdits(false);
1577
+ }
1578
+ }, [mode]);
1579
+
1580
+ const isReadOnly =
1581
+ !contentEditorItem ||
1582
+ !contentEditorItem.canWriteItem ||
1583
+ !contentEditorItem.canWriteLanguage ||
1584
+ !contentEditorItem.canWriteWorkflow;
1585
+
1586
+ const updateUrl = useCallback((params: Record<string, string>) => {
1587
+ const url = new URL(window.location.href);
1588
+ Object.entries(params).forEach(([key, value]) => {
1589
+ if (value) url.searchParams.set(key, value);
1590
+ else url.searchParams.delete(key);
1591
+ });
1592
+ router.push(url.toString(), { scroll: false });
1593
+ }, []);
1594
+
1595
+ // Quota checking functions
1596
+ const isQuotaExceeded = useCallback(() => {
1597
+ if (!quotaInfo) return false;
1598
+
1599
+ const { usage, limits } = quotaInfo;
1600
+
1601
+ // Check absolute limits
1602
+ if (limits.totalTokens > 0 && usage.totalTokens >= limits.totalTokens)
1603
+ return true;
1604
+ if (limits.totalImages > 0 && usage.totalImages >= limits.totalImages)
1605
+ return true;
1606
+
1607
+ // For now, we're only checking absolute limits as daily/monthly would require server-side logic
1608
+ // You can extend this to check daily/monthly limits if the server provides that information
1609
+
1610
+ return false;
1611
+ }, [quotaInfo]);
1612
+
1613
+ const getQuotaWarningMessage = useCallback(() => {
1614
+ if (!quotaInfo) return null;
1615
+
1616
+ const { usage, limits } = quotaInfo;
1617
+ const warnings: string[] = [];
1618
+
1619
+ // Check tokens
1620
+ if (limits.totalTokens > 0) {
1621
+ const tokenPercentage = (usage.totalTokens / limits.totalTokens) * 100;
1622
+ if (tokenPercentage >= 100) {
1623
+ warnings.push(
1624
+ `Token limit exceeded (${usage.totalTokens}/${limits.totalTokens})`,
1625
+ );
1626
+ } else if (tokenPercentage >= 90) {
1627
+ warnings.push(
1628
+ `Token usage high: ${Math.round(tokenPercentage)}% (${usage.totalTokens}/${limits.totalTokens})`,
1629
+ );
1630
+ }
1631
+ }
1632
+
1633
+ // Check images
1634
+ if (limits.totalImages > 0) {
1635
+ const imagePercentage = (usage.totalImages / limits.totalImages) * 100;
1636
+ if (imagePercentage >= 100) {
1637
+ warnings.push(
1638
+ `Image limit exceeded (${usage.totalImages}/${limits.totalImages})`,
1639
+ );
1640
+ } else if (imagePercentage >= 90) {
1641
+ warnings.push(
1642
+ `Image usage high: ${Math.round(imagePercentage)}% (${usage.totalImages}/${limits.totalImages})`,
1643
+ );
1644
+ }
1645
+ }
1646
+
1647
+ return warnings.length > 0 ? warnings.join(", ") : null;
1648
+ }, [quotaInfo]);
1649
+
1650
+ // Show warning when quota is exceeded
1651
+ useEffect(() => {
1652
+ const warningMessage = getQuotaWarningMessage();
1653
+ if (warningMessage) {
1654
+ const isExceeded = isQuotaExceeded();
1655
+ showErrorToast({
1656
+ summary: isExceeded ? "AI Quota Exceeded" : "AI Quota Warning",
1657
+ details: warningMessage,
1658
+ });
1659
+ }
1660
+ }, [quotaInfo, getQuotaWarningMessage, isQuotaExceeded, showErrorToast]);
1661
+
1662
+ const editContext = useMemo(() => {
1663
+ return {
1664
+ operations: operationsContext.ops,
1665
+ itemsRepository,
1666
+ configuration,
1667
+ updateUrl,
1668
+ openSplashScreen: () => {
1669
+ router.push("/alpaca/editor");
1670
+ },
1671
+ item: contentEditorItem || page?.item,
1672
+ itemLanguages,
1673
+ itemVersions,
1674
+ sessionId: sessionId,
1675
+ readonly: isReadOnly,
1676
+ selection,
1677
+ select: (ids: string[]) => {
1678
+ setSelection(ids);
1679
+ },
1680
+ selectedForInsertion,
1681
+ setSelectedForInsertion,
1682
+ dragObject,
1683
+ workboxItems,
1684
+ requestRefresh,
1685
+ refreshCompletedFlag,
1686
+ openCreatePageDialog: () => {
1687
+ const current = new URLSearchParams(Array.from(searchParams.entries()));
1688
+ current.delete("version");
1689
+ current.delete("itemid");
1690
+ current.delete("view");
1691
+ current.set("create", "1");
1692
+ const newUrl = `${pathname}?${current.toString()}`;
1693
+ router.push(newUrl, { scroll: false });
1694
+ },
1695
+ selectMedia,
1696
+ showToast: (message: ToastMessage | ToastMessage[]) => {
1697
+ toast.current?.show(message);
1698
+ },
1699
+ scrollIntoView,
1700
+ setScrollIntoView,
1701
+ focusedField,
1702
+ setFocusedField: async (
1703
+ field: FieldDescriptor | undefined,
1704
+ requestLock: boolean,
1705
+ ) => {
1706
+ if (field) {
1707
+ setIgnoreBlur(true);
1708
+ setTimeout(() => {
1709
+ setIgnoreBlur(false);
1710
+ }, 20);
1711
+ setFocusedField({ ...field });
1712
+ if (requestLock) {
1713
+ return (await operations.ensureLock(field)) || false;
1714
+ }
1715
+ } else {
1716
+ if (!ignoreBlur) {
1717
+ setFocusedField(undefined);
1718
+ releaseFieldLocks(sessionId);
1719
+ }
1720
+ }
1721
+ return true;
1722
+ },
1723
+ renderedFields,
1724
+ setRenderedFields,
1725
+ getComponentCommands: async (components: Component[]) => {
1726
+ return await getComponentCommands(components, editContext);
1727
+ },
1728
+ dragStart: (dragObject: DragObject) => {
1729
+ setDragObject(dragObject);
1730
+ },
1731
+ dragEnd: () => {
1732
+ setDragObject(undefined);
1733
+ },
1734
+ droppedInPlaceholder: async (
1735
+ placeholderKey: string,
1736
+ index: number,
1737
+ spotPositionElement?: Element,
1738
+ spotPositionAnchor?: "left" | "right" | "top" | "bottom",
1739
+ insertOption?: InsertOption,
1740
+ ) => {
1741
+ if ((!dragObject && !insertOption) || !page) return;
1742
+ setDragObject(undefined);
1743
+
1744
+ if (spotPositionElement && spotPositionAnchor) {
1745
+ setInserting({
1746
+ positionElement: spotPositionElement,
1747
+ positionAnchor: spotPositionAnchor,
1748
+ });
1749
+ }
1750
+
1751
+ const placeholderKeyComponents = placeholderKey.split("_");
1752
+ const parentId =
1753
+ placeholderKeyComponents.length === 1
1754
+ ? undefined
1755
+ : placeholderKeyComponents[1];
1756
+
1757
+ if (parentId) setSelection([parentId]);
1758
+
1759
+ let op: EditOperation | undefined;
1760
+
1761
+ if (!currentItemDescriptorRef.current) return;
1762
+
1763
+ if (insertOption) {
1764
+ operations.addComponent(
1765
+ insertOption.typeId,
1766
+ placeholderKey,
1767
+ index,
1768
+ currentItemDescriptorRef.current,
1769
+ );
1770
+ return;
1771
+ }
1772
+
1773
+ if (!dragObject) return;
1774
+
1775
+ if (dragObject && dragObject.type == "template") {
1776
+ operations.addComponent(
1777
+ dragObject.typeId,
1778
+ placeholderKey,
1779
+ index,
1780
+ currentItemDescriptorRef.current,
1781
+ );
1782
+ return;
1783
+ }
1784
+
1785
+ if (
1786
+ dragObject.type == "component" ||
1787
+ (dragObject.type == "link-component" && dragObject.component)
1788
+ ) {
1789
+ const parentComponent =
1790
+ parentId && page ? getComponentById(parentId, page) : null;
1791
+
1792
+ if (dragObject.type == "link-component") {
1793
+ op = {
1794
+ type: "link-component",
1795
+ mainItem: page!.item.descriptor,
1796
+ parent: parentId && {
1797
+ id: parentId,
1798
+ language: page!.item.descriptor.language,
1799
+ version: page!.item.descriptor.version,
1800
+ name: parentComponent?.name,
1801
+ },
1802
+ placeholderKey,
1803
+ placeholderIndex: index,
1804
+ date: new Date().toISOString(),
1805
+ id: uuid(),
1806
+ linkedComponentItem: dragObject.component,
1807
+ description: "Link component",
1808
+ } as LinkComponentOperation;
1809
+ } else {
1810
+ if (!dragObject.component) return;
1811
+ op = {
1812
+ type: "move-component",
1813
+ mainItem: page!.item.descriptor,
1814
+ parent: parentId && {
1815
+ id: parentId,
1816
+ language: page!.item.descriptor.language,
1817
+ version: page!.item.descriptor.version,
1818
+ },
1819
+ placeholderKey,
1820
+ placeholderIndex: index,
1821
+ componentIds: [dragObject.component.id],
1822
+ date: new Date().toISOString(),
1823
+ id: uuid(),
1824
+ description: "Move component",
1825
+ } as MoveComponentOperation;
1826
+ }
1827
+ }
1828
+
1829
+ if (op) operations.executeEditOperation(op);
1830
+ },
1831
+ page,
1832
+ triggerFieldAction: async (
1833
+ fieldDescriptor: FieldDescriptor,
1834
+ actionButton: FieldButton,
1835
+ parameters?: Record<string, string>,
1836
+ ) => {
1837
+ const field = await itemsRepository.getField(fieldDescriptor);
1838
+
1839
+ if (!field) return;
1840
+
1841
+ const op: FieldAction = {
1842
+ field: fieldDescriptor,
1843
+ // actionButton,
1844
+ label: actionButton.label,
1845
+ state: "running",
1846
+ };
1847
+ const fieldItem = fieldDescriptor.item;
1848
+
1849
+ if ("clientAction" in actionButton) {
1850
+ await (actionButton as ClientFieldButton).clientAction!({
1851
+ field,
1852
+ editContext,
1853
+ //dialogContext,
1854
+ });
1855
+ }
1856
+
1857
+ if (actionButton.action) {
1858
+ setActiveFieldActions((prevFieldActions) => [
1859
+ ...prevFieldActions.filter(
1860
+ (x) =>
1861
+ !(
1862
+ x.field.fieldId == fieldDescriptor.fieldId &&
1863
+ x.field.item.id == fieldItem.id &&
1864
+ x.field.item.language == fieldItem.language &&
1865
+ x.field.item.version == fieldItem.version
1866
+ ),
1867
+ ),
1868
+ op,
1869
+ ]);
1870
+ await executeFieldServerAction(
1871
+ fieldDescriptor.item,
1872
+ fieldDescriptor.fieldId,
1873
+ contentEditorItem!.descriptor,
1874
+ actionButton.action,
1875
+ editContext!.sessionId,
1876
+ selectedRange?.text || "",
1877
+ parameters || {},
1878
+ (data: any) => {
1879
+ op.message = data.responseText;
1880
+ setActiveFieldActions((prevFieldActions) => [
1881
+ ...prevFieldActions,
1882
+ ]);
1883
+ },
1884
+ );
1885
+
1886
+ itemsRepository.refreshItems([fieldDescriptor.item]);
1887
+
1888
+ op.state = "success";
1889
+ requestRefresh("immediate");
1890
+ }
1891
+ },
1892
+ activeFieldActions,
1893
+ showContextMenu: (event: any, items: MenuItem[]) => {
1894
+ contextMenuRef.current?.show(event, items);
1895
+ setCurrentOverlay("context-menu");
1896
+ },
1897
+ showAiPopup: (
1898
+ event: MouseEvent<HTMLElement>,
1899
+ aiTerminalOptions?: AiTerminalOptions,
1900
+ ) => {
1901
+ setCurrentOverlay("ai");
1902
+ aiPopupRef.current?.show(event, aiTerminalOptions);
1903
+ },
1904
+ showFieldEditorPopup: (fields: Field[], sections: string[], ev: any) => {
1905
+ setCurrentOverlay("fields");
1906
+ fieldEditorPopupRef.current?.show(fields, sections, ev);
1907
+ },
1908
+ inserting,
1909
+ validating,
1910
+ validationResult,
1911
+ contentEditorItem,
1912
+ loadItem,
1913
+ editHistory,
1914
+ isRefreshing,
1915
+ activeSessions,
1916
+ unlockField: async () => {
1917
+ await releaseFieldLocks(sessionId);
1918
+ },
1919
+ isCommandDisabled: <T extends CommandData>({
1920
+ command,
1921
+ data,
1922
+ }: {
1923
+ command: Command<T>;
1924
+ data?: T;
1925
+ }): boolean => {
1926
+ const props = {
1927
+ editContext,
1928
+ pathname,
1929
+ router,
1930
+ searchParams,
1931
+ data,
1932
+ openDialog,
1933
+ };
1934
+
1935
+ return command.disabled(props);
1936
+ },
1937
+ executeCommand,
1938
+ viewName,
1939
+ switchView,
1940
+ compareMode,
1941
+ setCompareMode,
1942
+ view: currentView,
1943
+ pageView: pageViewContext,
1944
+ componentDesignerComponent,
1945
+ setComponentDesignerComponent,
1946
+ componentDesignerRendering,
1947
+ setComponentDesignerRendering,
1948
+ insertMode,
1949
+ setInsertMode,
1950
+ currentOverlay,
1951
+ setCurrentOverlay,
1952
+ inlineEditingFieldElement,
1953
+ setInlineEditingFieldElement,
1954
+ lockedField,
1955
+ timings,
1956
+ setTimings,
1957
+ selectedRange,
1958
+ setSelectedRange,
1959
+ confirmationDialogRef,
1960
+ confirm: (props: ConfirmationProps) => {
1961
+ confirmationDialogRef.current?.confirm(props);
1962
+ },
1963
+ showMessageDialog: (props: { header: string; message: string }) => {
1964
+ confirmationDialogRef.current?.confirm({
1965
+ header: props.header,
1966
+ message: props.message,
1967
+ accept: () => {},
1968
+ acceptLabel: "Ok",
1969
+ });
1970
+ },
1971
+ browseHistory,
1972
+ setCenterPanelView,
1973
+ handleKeyDown,
1974
+ startTour,
1975
+ addSocketMessageListener,
1976
+ currentItemDescriptor,
1977
+ compareTo,
1978
+ setCompareTo,
1979
+ lastEditedFields,
1980
+ revision,
1981
+ selectedComment,
1982
+ setSelectedComment,
1983
+ comments,
1984
+ loadComments,
1985
+ setComments,
1986
+ showComments,
1987
+ setShowComments,
1988
+ addComment: async () => {
1989
+ const descriptor = contentEditorItem?.descriptor;
1990
+ if (!descriptor) return;
1991
+
1992
+ const itemId =
1993
+ focusedField?.item.id ||
1994
+ (selection.length > 0 ? selection[0] : undefined) ||
1995
+ descriptor.id;
1996
+
1997
+ if (!itemId) return;
1998
+
1999
+ const language = descriptor.language;
2000
+ const version = descriptor.version;
2001
+
2002
+ const getFieldName = async () => {
2003
+ if (!focusedField) return "";
2004
+ const field = await itemsRepository.getField(focusedField);
2005
+ return field?.name;
2006
+ };
2007
+
2008
+ const getItemName = async () => {
2009
+ const item = await itemsRepository.getItem({
2010
+ id: itemId,
2011
+ language,
2012
+ version,
2013
+ });
2014
+ return item?.name;
2015
+ };
2016
+
2017
+ const newComment = {
2018
+ id: uuid(),
2019
+ isNew: true,
2020
+ itemId,
2021
+ itemName: await getItemName(),
2022
+ fieldId: focusedField?.fieldId,
2023
+ fieldName: await getFieldName(),
2024
+ mainItemId: descriptor.id,
2025
+ language,
2026
+ version,
2027
+ position: 0,
2028
+ rangeStart: selectedRange?.startOffset || 0,
2029
+ rangeEnd: selectedRange?.endOffset || 0,
2030
+ author: user?.name,
2031
+ authorDisplayName: user?.displayName,
2032
+ date: new Date().toISOString(),
2033
+ };
2034
+
2035
+ setComments([newComment, ...comments]);
2036
+ setSelectedComment(newComment);
2037
+ setShowComments(true);
2038
+ },
2039
+ mode,
2040
+ setMode,
2041
+
2042
+ user,
2043
+ reviews,
2044
+ statusMessage,
2045
+ setStatusMessage,
2046
+ suggestedEdits,
2047
+ showSuggestedEdits,
2048
+ setShowSuggestedEdits,
2049
+ showSuggestedEditsDiff,
2050
+ setShowSuggestedEditsDiff,
2051
+ enableCompletions,
2052
+ setEnableCompletions,
2053
+ quotaInfo,
2054
+ isQuotaExceeded: isQuotaExceeded(),
2055
+ getQuotaWarningMessage,
2056
+ isMobile,
2057
+ openDialog,
2058
+ pageWizard,
2059
+ webSocketMessages,
2060
+ clearWebSocketMessages: () => setWebSocketMessages([]),
2061
+ userInfo,
2062
+ };
2063
+ }, [
2064
+ operations,
2065
+ itemsRepository,
2066
+ configuration,
2067
+ contentEditorItem,
2068
+ page?.item,
2069
+ itemLanguages,
2070
+ itemVersions,
2071
+ sessionId,
2072
+ isReadOnly,
2073
+ selection,
2074
+ selectedForInsertion,
2075
+ dragObject,
2076
+ requestRefresh,
2077
+ refreshCompletedFlag,
2078
+ searchParams,
2079
+ pathname,
2080
+ router,
2081
+ selectMedia,
2082
+ scrollIntoView,
2083
+ focusedField,
2084
+ renderedFields,
2085
+ inserting,
2086
+ page,
2087
+ activeFieldActions,
2088
+ editHistory,
2089
+ isRefreshing,
2090
+ activeSessions,
2091
+ currentView,
2092
+ componentDesignerComponent,
2093
+ componentDesignerRendering,
2094
+ insertMode,
2095
+ currentOverlay,
2096
+ inlineEditingFieldElement,
2097
+ lockedField,
2098
+ selectedRange,
2099
+ pageViewContext,
2100
+ browseHistory,
2101
+ workboxItems,
2102
+ validating,
2103
+ setCenterPanelView,
2104
+ handleKeyDown,
2105
+ setTimings,
2106
+ timings,
2107
+ startTour,
2108
+ viewName,
2109
+ compareMode,
2110
+ setCompareMode,
2111
+ addSocketMessageListener,
2112
+ currentItemDescriptor,
2113
+ compareTo,
2114
+ setCompareTo,
2115
+ lastEditedFields,
2116
+ revision,
2117
+ comments,
2118
+ setComments,
2119
+ selectedComment,
2120
+ setSelectedComment,
2121
+ loadComments,
2122
+ mode,
2123
+ setMode,
2124
+ user,
2125
+ reviews,
2126
+ statusMessage,
2127
+ setStatusMessage,
2128
+ suggestedEdits,
2129
+ setSuggestedEdits,
2130
+ showSuggestedEdits,
2131
+ setShowSuggestedEdits,
2132
+ showSuggestedEditsDiff,
2133
+ setShowSuggestedEditsDiff,
2134
+ quotaInfo,
2135
+ isQuotaExceeded,
2136
+ getQuotaWarningMessage,
2137
+ isMobile,
2138
+ openDialog,
2139
+ pageWizard,
2140
+ webSocketMessages,
2141
+ userInfo,
2142
+ ]);
2143
+
2144
+ const modifiedFieldsContext = useMemo(() => {
2145
+ return {
2146
+ modifiedFields,
2147
+ clear: () => {
2148
+ setModifiedFields([]);
2149
+ },
2150
+ };
2151
+ }, [modifiedFields, setModifiedFields]);
2152
+
2153
+ useEffect(() => {
2154
+ editContextRef.current = editContext;
2155
+ }, [editContext]);
2156
+
2157
+ useEffect(() => {
2158
+ modifiedFieldsContext.clear();
2159
+ }, [currentItemDescriptor]);
2160
+
2161
+ if (!currentView) return null;
2162
+
2163
+ return (
2164
+ <div className={`editor h-full`}>
2165
+ <OperationsContextProvider value={operationsContext.context}>
2166
+ <ModifiedFieldsContextProvider value={modifiedFieldsContext}>
2167
+ <EditContextProvider value={editContext}>
2168
+ {pageViewContext.fullscreen ? (
2169
+ <>
2170
+ <div className="fixed inset-0 flex">
2171
+ <PageViewerFrame
2172
+ compareView={compareMode}
2173
+ pageViewContext={pageViewContext}
2174
+ />
2175
+ </div>
2176
+ {showFullscreenHint && (
2177
+ <div
2178
+ className="fixed inset-0"
2179
+ onMouseMoveCapture={() => {
2180
+ setTimeout(() => {
2181
+ setShowFullscreenHint(false);
2182
+ }, 600);
2183
+ }}
2184
+ >
2185
+ <div className="fixed top-3 left-1/2 -translate-x-1/2 transform rounded-sm bg-gray-200 p-12">
2186
+ Press Ctrl + F11 to exit fullscreen mode
2187
+ </div>
2188
+ </div>
2189
+ )}
2190
+ </>
2191
+ ) : (
2192
+ <>
2193
+ <Toast ref={toast} />
2194
+ <ConfirmationDialog ref={confirmationDialogRef} />
2195
+ <EditContextMenu ref={contextMenuRef} />
2196
+ <MainLayout
2197
+ className={className}
2198
+ view={currentView}
2199
+ centerPanelView={centerPanelView}
2200
+ rightSidebar={
2201
+ currentView.rightSidebar && (
2202
+ <SidebarView
2203
+ sidebar={currentView.rightSidebar}
2204
+ editContext={editContext}
2205
+ active={true}
2206
+ />
2207
+ )
2208
+ }
2209
+ rightSidebarTitle={currentView.rightSidebar?.title}
2210
+ />
2211
+
2212
+ {mediaSelectorVisible && (
2213
+ <MediaSelector
2214
+ language={editContext.currentItemDescriptor!.language}
2215
+ visible={mediaSelectorVisible}
2216
+ onHide={() => setMediaSelectorVisible(false)}
2217
+ onMediaSelected={onMediaSelect}
2218
+ selectedIdPath={selectedMediaIdPath}
2219
+ mode={mediaSelectorMode}
2220
+ />
2221
+ )}
2222
+
2223
+ <AiPopup ref={aiPopupRef} />
2224
+ <FieldEditorPopup ref={fieldEditorPopupRef} />
2225
+ {isTourActive && (
2226
+ <Tour tourStopCallback={() => setIsTourActive(false)} />
2227
+ )}
2228
+ </>
2229
+ )}
2230
+ {editContext.isRefreshing && (
2231
+ <div className="pointer-events-none fixed right-0 bottom-0 flex h-24 w-24 items-center justify-center text-gray-600 opacity-50 select-none">
2232
+ <Spinner />
2233
+ </div>
2234
+ )}
2235
+ {dialog}
2236
+ </EditContextProvider>
2237
+ </ModifiedFieldsContextProvider>
2238
+ </OperationsContextProvider>
2239
+ </div>
2240
+ );
2241
+ }