@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,610 @@
1
+ import uuid from "react-uuid";
2
+ import {
3
+ AddComponentOperation,
4
+ DuplicateComponentsOperation,
5
+ EditFieldOperation,
6
+ EditOperation,
7
+ ExecuteEditOptions,
8
+ RenameItemOperation,
9
+ User,
10
+ } from "../../types";
11
+ import { FieldDescriptor, FullItem, ItemDescriptor, Page } from "../pageModel";
12
+ import {
13
+ executeEditOperation,
14
+ executeRedo,
15
+ executeUndo,
16
+ executeWorkflowCommand,
17
+ executeDeleteItems,
18
+ ExecutionResult,
19
+ lockField,
20
+ lockItems,
21
+ unlockItems,
22
+ executeCreateItem,
23
+ createVersion as doCreateVersion,
24
+ executeMoveItems,
25
+ } from "../services/editService";
26
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
27
+ import { EditorConfiguration } from "../../config/types";
28
+ import { ConfirmationDialogHandle } from "../ConfirmationDialog";
29
+ import { handleErrorResult } from "./helpers";
30
+ import { ItemsRepository } from "./itemsRepository";
31
+ import { useDebouncedCallback } from "use-debounce";
32
+
33
+ export function getOperationsContext(
34
+ state: {
35
+ page?: Page;
36
+ contentEditorItem?: FullItem;
37
+ sessionId: string;
38
+ user?: User;
39
+ setSelection: (ids: string[]) => void;
40
+ requestRefresh: (refresh: "immediate" | "waitForQuietPeriod") => void;
41
+ configuration: EditorConfiguration;
42
+ setLockedField: (field: FieldDescriptor) => void;
43
+ lockedField?: FieldDescriptor;
44
+ loadItem: (item: ItemDescriptor) => void;
45
+ itemsRepository: ItemsRepository;
46
+ editHistory: EditOperation[];
47
+ refreshHistory: (item: ItemDescriptor) => Promise<void>;
48
+ },
49
+ ui: {
50
+ showErrorToast: (error: { summary?: string; details?: string }) => void;
51
+ confirmationDialogRef: React.RefObject<ConfirmationDialogHandle | null>;
52
+ onOperationExecuted: (op: EditOperation) => void;
53
+ },
54
+ ) {
55
+ const [editOperationExecuted, setEditOperationExecuted] =
56
+ useState<EditOperation>();
57
+ const [executingEditOperations, setExecutingEditOperations] = useState<
58
+ EditOperation[]
59
+ >([]);
60
+
61
+ const lastOp = useRef<EditOperation | undefined>(undefined);
62
+
63
+ const undoing = useRef<boolean>(false);
64
+
65
+ const stateRef = useRef(state);
66
+
67
+ useEffect(() => {
68
+ stateRef.current = state;
69
+ }, [state]);
70
+
71
+ const executeOp = async (
72
+ op: EditOperation,
73
+ options: ExecuteEditOptions = {
74
+ refresh: "immediate",
75
+ },
76
+ ): Promise<EditOperation> => {
77
+ console.log("Executing op", op);
78
+ setExecutingEditOperations((ops) => (ops ? [...ops, op] : [op]));
79
+ const result = await executeEditOperation(op, state.sessionId);
80
+ console.log("Result", result);
81
+ handleResult(result, options);
82
+ await state.refreshHistory(op.mainItem);
83
+ //state.addToEditHistory(result.data as EditOperation);
84
+ setEditOperationExecuted(result.data as EditOperation);
85
+ setExecutingEditOperations((ops) =>
86
+ ops ? ops.filter((x: EditOperation) => x.id !== op.id) : [],
87
+ );
88
+ return result.data as EditOperation;
89
+ };
90
+
91
+ const duplicateComponents = useCallback(
92
+ ({ componentIds }: { componentIds: string[] }) => {
93
+ const op = {
94
+ type: "duplicate-components",
95
+ mainItem: state.page?.item.descriptor,
96
+ sourceComponentIds: componentIds,
97
+ date: new Date().toISOString(),
98
+ id: uuid(),
99
+ description: "Duplicate components",
100
+ } as DuplicateComponentsOperation;
101
+
102
+ executeOp(op);
103
+ },
104
+ [state.page],
105
+ );
106
+
107
+ const addComponent = async (
108
+ componentTypeId: string,
109
+ placeholderKey: string,
110
+ index: number,
111
+ ) => {
112
+ const page = stateRef.current.page;
113
+
114
+ console.log(
115
+ " component A",
116
+ componentTypeId,
117
+ placeholderKey,
118
+ index,
119
+ "PAGE ",
120
+ page,
121
+ );
122
+
123
+ if (!page) {
124
+ return;
125
+ }
126
+
127
+ const op: AddComponentOperation = {
128
+ type: "add-component",
129
+ mainItem: page.item.descriptor,
130
+ // parent: parentId
131
+ // ? {
132
+ // id: parentId,
133
+ // language: page.item.language,
134
+ // version: page.item.version,
135
+ // }
136
+ // : undefined,
137
+ placeholderKey,
138
+ placeholderIndex: index,
139
+ componentTypeId,
140
+ date: new Date().toISOString(),
141
+ id: uuid(),
142
+ description: "Add component",
143
+ };
144
+ const result = await executeOp(op);
145
+ return result as AddComponentOperation;
146
+ };
147
+
148
+ const handleResult = (
149
+ result: ExecutionResult<unknown>,
150
+ options: ExecuteEditOptions,
151
+ ) => {
152
+ if (handleErrorResult(result as ExecutionResult<unknown>, ui, state))
153
+ return;
154
+
155
+ const executedOps = Array.isArray(result.data)
156
+ ? result.data
157
+ : [result.data];
158
+
159
+ for (const executedOp of executedOps) {
160
+ if (
161
+ executedOp &&
162
+ (executedOp.type === "move-component" ||
163
+ executedOp.type === "add-component" ||
164
+ executedOp.type === "link-component" ||
165
+ executedOp.type === "duplicate-components")
166
+ ) {
167
+ if (executedOp.focus) {
168
+ state.setSelection([executedOp.focus]);
169
+ }
170
+ }
171
+
172
+ ui.onOperationExecuted(executedOp);
173
+ }
174
+
175
+ if (options.refresh !== "none") {
176
+ state.requestRefresh(
177
+ options.refresh === "immediate" ? "immediate" : "waitForQuietPeriod",
178
+ );
179
+ }
180
+ };
181
+
182
+ const ensureLock = useCallback(
183
+ async (field: FieldDescriptor): Promise<boolean> => {
184
+ if (!field || !field.item) return false;
185
+ if (
186
+ state.lockedField?.fieldId === field.fieldId &&
187
+ state.lockedField?.item.id === field.item.id &&
188
+ state.lockedField?.item.language === field.item.language &&
189
+ state.lockedField?.item.version === field.item.version
190
+ )
191
+ return true;
192
+ const result = await lockField(
193
+ field.item,
194
+ field.fieldId,
195
+ state.sessionId,
196
+ );
197
+ if (result.type == "error") {
198
+ console.log("error locking field", result);
199
+ }
200
+ //handleErrorResult(result, ui, state);
201
+ if (result.type == "success" && result.data.success) {
202
+ state.setLockedField(field);
203
+ return true;
204
+ }
205
+ return false;
206
+ },
207
+ [state, ui],
208
+ );
209
+
210
+ const lockItemsAndRefresh = useCallback(
211
+ async (items: ItemDescriptor[]) => {
212
+ const item = items[0];
213
+ if (!item) return;
214
+ await lockItems(items);
215
+ if (item.id == state.contentEditorItem?.id) {
216
+ await state.loadItem(item);
217
+ } else state.requestRefresh("immediate");
218
+ },
219
+ [state],
220
+ );
221
+
222
+ const unlockItemsAndRefresh = useCallback(
223
+ async (items: ItemDescriptor[]) => {
224
+ const item = items[0];
225
+ if (!item) return;
226
+ await unlockItems(items);
227
+ if (item.id == state.contentEditorItem?.id) {
228
+ await state.loadItem(item);
229
+ } else state.requestRefresh("immediate");
230
+ },
231
+ [state],
232
+ );
233
+
234
+ const executeWorkflowCommandAndRefresh = useCallback(
235
+ async (item: ItemDescriptor, commandId: string) => {
236
+ const result = await executeWorkflowCommand(item, commandId, "");
237
+ if (result.type === "success") {
238
+ if (!result.data.succeeded) {
239
+ const errorMessage = {
240
+ summary: "Workflow command failed",
241
+ details: result.data.message,
242
+ };
243
+ ui.showErrorToast(errorMessage);
244
+ }
245
+ state.requestRefresh("immediate");
246
+ }
247
+ },
248
+ [state],
249
+ );
250
+
251
+ const renameItem = useCallback(
252
+ async (item: ItemDescriptor, newName: string) => {
253
+ await executeOp({
254
+ type: "rename-item",
255
+ mainItem: state.page?.item.descriptor || item,
256
+ date: new Date().toISOString(),
257
+ id: uuid(),
258
+ item,
259
+ newName,
260
+ description: "Rename item",
261
+ } as RenameItemOperation);
262
+ state.requestRefresh("immediate");
263
+ state.itemsRepository.refreshItems([item]);
264
+ },
265
+ [state.itemsRepository, executeOp],
266
+ );
267
+
268
+ const lastEditField = useRef<FieldDescriptor | undefined>(undefined);
269
+
270
+ const editField = useCallback(
271
+ async ({
272
+ field,
273
+ value,
274
+ rawValue,
275
+ refresh = "immediate",
276
+ }: {
277
+ field: FieldDescriptor;
278
+ value?: string;
279
+ rawValue?: string | null;
280
+ refresh?: "none" | "immediate" | "delayed" | "waitForQuietPeriod";
281
+ }): Promise<void> => {
282
+ state.itemsRepository.updateFieldValue(
283
+ field,
284
+ state.user ?? { name: "unknown", ai: false },
285
+ true,
286
+ value,
287
+ rawValue,
288
+ );
289
+ if (refresh === "immediate" || refresh === "none") {
290
+ return editFieldImmediate({ field, value, rawValue, refresh });
291
+ }
292
+ if (
293
+ lastEditField.current?.fieldId !== field.fieldId ||
294
+ lastEditField.current?.item?.id !== field.item?.id ||
295
+ lastEditField.current?.item?.language !== field.item?.language ||
296
+ lastEditField.current?.item?.version !== field.item?.version
297
+ ) {
298
+ executeEditFieldDebounced.flush();
299
+ }
300
+ lastEditField.current = field;
301
+ return executeEditFieldDebounced({ field, value, rawValue, refresh });
302
+ },
303
+ [state.itemsRepository],
304
+ );
305
+
306
+ const editFieldImmediate = useCallback(
307
+ async ({
308
+ field,
309
+ value,
310
+ rawValue,
311
+ refresh = "immediate",
312
+ }: {
313
+ field: FieldDescriptor;
314
+ value?: string;
315
+ rawValue?: string | null;
316
+ refresh?: "none" | "immediate" | "delayed" | "waitForQuietPeriod";
317
+ }): Promise<void> => {
318
+ state.itemsRepository.updateFieldValue(
319
+ field,
320
+ state.user ?? { name: "unknown", ai: false },
321
+ true,
322
+ value,
323
+ rawValue,
324
+ );
325
+
326
+ const val = rawValue !== undefined ? rawValue : value;
327
+ if (val === undefined) return;
328
+
329
+ const op = await getEditOP(field, rawValue, value);
330
+
331
+ if (op) {
332
+ await executeOp(op, { refresh });
333
+ await state.itemsRepository.refreshItems([field.item]);
334
+ state.itemsRepository.onFieldSaved(field, val);
335
+ }
336
+ },
337
+ [state.itemsRepository, executeOp],
338
+ );
339
+
340
+ const executeEditFieldDebounced = useDebouncedCallback(
341
+ editFieldImmediate,
342
+ state.configuration.debounceFieldEditsInterval * 2,
343
+ { trailing: true },
344
+ );
345
+
346
+ async function getEditOP(
347
+ field: FieldDescriptor,
348
+ rawValue: string | null | undefined,
349
+ value: string | undefined,
350
+ ) {
351
+ const item = await state.itemsRepository.getItem(field.item);
352
+ if (!item) return;
353
+
354
+ const fieldItem = await state.itemsRepository.getItem({
355
+ id: field.fieldId,
356
+ language: "en",
357
+ version: 0,
358
+ });
359
+
360
+ if (!fieldItem) {
361
+ console.log("Could not fetch field item " + field.fieldId);
362
+ return;
363
+ }
364
+
365
+ let op = lastOp.current as EditFieldOperation;
366
+
367
+ if (
368
+ !op ||
369
+ op.undone ||
370
+ op.type !== "edit-field" ||
371
+ !op.item ||
372
+ op.item.id !== item.id ||
373
+ op.fieldId !== field.fieldId ||
374
+ op.item.language !== item.language ||
375
+ op.item.version !== item.version
376
+ ) {
377
+ op = {
378
+ type: "edit-field",
379
+ item: {
380
+ id: item.id,
381
+ version: item.version,
382
+ language: item.language,
383
+ name: item.name,
384
+ },
385
+ fieldId: field.fieldId,
386
+ fieldName: fieldItem.name,
387
+ date: new Date().toISOString(),
388
+ id: uuid(),
389
+ description: "Edit " + item.name,
390
+ focus: item.id + ":" + field.fieldId,
391
+ canUndo: true,
392
+ mainItem: state.page?.item.descriptor || item.descriptor,
393
+ };
394
+ }
395
+
396
+ op.date = new Date().toISOString();
397
+ op.value = rawValue ?? value;
398
+
399
+ const firstEditInheritedComponent =
400
+ item.isInheritedFromMasterLanguage &&
401
+ op.item.language != state.page?.item.language;
402
+
403
+ if (firstEditInheritedComponent)
404
+ op.item.language = state.page!.item.language;
405
+
406
+ lastOp.current = op;
407
+ return op;
408
+ }
409
+
410
+ const undo = useCallback(
411
+ async (numberOfOperations?: number): Promise<boolean> => {
412
+ if (undoing.current) {
413
+ return false;
414
+ }
415
+ undoing.current = true;
416
+
417
+ try {
418
+ const item = state.contentEditorItem?.descriptor;
419
+ if (!item) return false;
420
+
421
+ const ops = state.editHistory
422
+ .filter((x) => x.canUndo && x.sessionId === state.sessionId)
423
+ .slice(0, numberOfOperations || 1);
424
+
425
+ if (!ops.length) return false;
426
+
427
+ // Mark operations as undone
428
+ ops.forEach((x) => {
429
+ x.undone = true;
430
+ x.canUndo = false;
431
+ });
432
+
433
+ const result = await executeUndo(
434
+ state.sessionId,
435
+ ops.map((x) => x.id),
436
+ );
437
+ handleResult(result, { refresh: "immediate" });
438
+
439
+ if (result.type === "success") {
440
+ const itemsToRefresh = result.data
441
+ .map((x: EditOperation) => {
442
+ if (x.type === "edit-field")
443
+ return (x as EditFieldOperation).item;
444
+ if (x.type === "add-component")
445
+ return {
446
+ id: (x as AddComponentOperation).componentId,
447
+ language: x.mainItem.language,
448
+ version: x.mainItem.version,
449
+ };
450
+ return undefined;
451
+ })
452
+ .filter(
453
+ (x: ItemDescriptor | undefined): x is ItemDescriptor => x != null,
454
+ );
455
+
456
+ await state.itemsRepository.refreshItems(itemsToRefresh);
457
+ await state.refreshHistory(item);
458
+ return true;
459
+ } else {
460
+ ui.showErrorToast(result);
461
+ return false;
462
+ }
463
+ } finally {
464
+ // Always reset the flag
465
+ undoing.current = false;
466
+ console.log("undoing flag reset");
467
+ }
468
+ },
469
+ [state, ui, undoing],
470
+ );
471
+
472
+ const redo = useCallback(async (): Promise<boolean> => {
473
+ if (undoing.current) return false;
474
+ undoing.current = true;
475
+
476
+ try {
477
+ const item = state.contentEditorItem?.descriptor;
478
+ if (!item) return false;
479
+
480
+ // Clone the array to avoid mutating the original state
481
+ const op = [...state.editHistory]
482
+ .reverse()
483
+ .find((x) => x.canRedo && x.sessionId === state.sessionId);
484
+ if (!op) return false;
485
+
486
+ op.canRedo = false;
487
+ op.undone = false;
488
+
489
+ const result = await executeRedo(state.sessionId, op.id);
490
+ handleResult(result, { refresh: "immediate" });
491
+
492
+ if (result.type === "success") {
493
+ const itemsToRefresh = result.data
494
+ .map((x: EditOperation) => {
495
+ if (x.type === "edit-field") return (x as EditFieldOperation).item;
496
+ if (x.type === "add-component")
497
+ return {
498
+ id: (x as AddComponentOperation).componentId,
499
+ language: x.mainItem.language,
500
+ version: x.mainItem.version,
501
+ };
502
+ return undefined;
503
+ })
504
+ .filter(
505
+ (x: ItemDescriptor | undefined): x is ItemDescriptor => x != null,
506
+ );
507
+
508
+ await state.itemsRepository.refreshItems(itemsToRefresh);
509
+ await state.refreshHistory(item);
510
+ return true;
511
+ } else {
512
+ ui.showErrorToast(result);
513
+ return false;
514
+ }
515
+ } finally {
516
+ // Always reset the flag
517
+ undoing.current = false;
518
+ }
519
+ }, [state, ui, undoing]);
520
+
521
+ const deleteItems = useCallback(
522
+ async (items: ItemDescriptor[]) => {
523
+ const itemsToDelete = await state.itemsRepository.getItems(items);
524
+ await executeDeleteItems(items);
525
+ state.itemsRepository.onItemsDeleted(
526
+ itemsToDelete.map((x) => ({
527
+ item: x.descriptor,
528
+ parentId: x.parentId,
529
+ })),
530
+ );
531
+ state.requestRefresh("immediate");
532
+ },
533
+ [state.itemsRepository],
534
+ );
535
+
536
+ const moveItems = useCallback(
537
+ async (items: ItemDescriptor[], target: ItemDescriptor, index: number) => {
538
+ await executeMoveItems(items, target, index);
539
+ },
540
+ [state.itemsRepository],
541
+ );
542
+
543
+ const createItem = useCallback(
544
+ async (parent: ItemDescriptor, templateId: string, name: string) => {
545
+ const result = await executeCreateItem(parent, templateId, name);
546
+ if (handleErrorResult(result as ExecutionResult<unknown>, ui, state))
547
+ return;
548
+ state.itemsRepository.refreshItems([parent]);
549
+ return result.data as ItemDescriptor;
550
+ },
551
+ [state.itemsRepository],
552
+ );
553
+
554
+ const createVersion = useCallback(
555
+ async (item: ItemDescriptor) => {
556
+ await doCreateVersion(item, state.sessionId);
557
+ },
558
+ [state],
559
+ );
560
+
561
+ const ops = useMemo(
562
+ () => ({
563
+ addComponent,
564
+ ensureLock,
565
+ lockItems: lockItemsAndRefresh,
566
+ unlockItems: unlockItemsAndRefresh,
567
+ executeWorkflowCommand: executeWorkflowCommandAndRefresh,
568
+ editField,
569
+ undo,
570
+ redo,
571
+ executeEditOperation: executeOp,
572
+ renameItem,
573
+ deleteItems,
574
+ createItem,
575
+ createVersion,
576
+ duplicateComponents,
577
+ undoing: undoing.current,
578
+ moveItems,
579
+ }),
580
+ [
581
+ addComponent,
582
+ ensureLock,
583
+ lockItemsAndRefresh,
584
+ unlockItemsAndRefresh,
585
+ executeWorkflowCommandAndRefresh,
586
+ editField,
587
+ undo,
588
+ redo,
589
+ executeOp,
590
+ renameItem,
591
+ deleteItems,
592
+ createItem,
593
+ createVersion,
594
+ duplicateComponents,
595
+ undoing.current,
596
+ moveItems,
597
+ ],
598
+ );
599
+
600
+ return useMemo(
601
+ () => ({
602
+ ops,
603
+ context: {
604
+ editOperationExecuted,
605
+ executingEditOperations,
606
+ },
607
+ }),
608
+ [executingEditOperations, editOperationExecuted, ops, undoing],
609
+ );
610
+ }