@alpaca-editor/core 1.0.0

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