@alpaca-editor/core 1.0.4073 → 1.0.4074

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 (58) hide show
  1. package/dist/editor/Editor.js +3 -3
  2. package/dist/editor/Editor.js.map +1 -1
  3. package/dist/editor/client/{EditorClient.d.ts → EditorShell.d.ts} +1 -19
  4. package/dist/editor/client/{EditorClient.js → EditorShell.js} +55 -572
  5. package/dist/editor/client/EditorShell.js.map +1 -0
  6. package/dist/editor/client/editContext.d.ts +2 -1
  7. package/dist/editor/client/hooks/useEditorUrlSync.d.ts +18 -0
  8. package/dist/editor/client/hooks/useEditorUrlSync.js +56 -0
  9. package/dist/editor/client/hooks/useEditorUrlSync.js.map +1 -0
  10. package/dist/editor/client/hooks/useEditorWebSocket.d.ts +11 -0
  11. package/dist/editor/client/hooks/useEditorWebSocket.js +70 -0
  12. package/dist/editor/client/hooks/useEditorWebSocket.js.map +1 -0
  13. package/dist/editor/client/hooks/useGlobalEditorEvents.d.ts +4 -0
  14. package/dist/editor/client/hooks/useGlobalEditorEvents.js +15 -0
  15. package/dist/editor/client/hooks/useGlobalEditorEvents.js.map +1 -0
  16. package/dist/editor/client/hooks/useMediaQuery.d.ts +1 -0
  17. package/dist/editor/client/hooks/useMediaQuery.js +19 -0
  18. package/dist/editor/client/hooks/useMediaQuery.js.map +1 -0
  19. package/dist/editor/client/hooks/useMediaSelector.d.ts +12 -0
  20. package/dist/editor/client/hooks/useMediaSelector.js +30 -0
  21. package/dist/editor/client/hooks/useMediaSelector.js.map +1 -0
  22. package/dist/editor/client/hooks/useQuota.d.ts +29 -0
  23. package/dist/editor/client/hooks/useQuota.js +53 -0
  24. package/dist/editor/client/hooks/useQuota.js.map +1 -0
  25. package/dist/editor/client/hooks/useSocketMessageHandler.d.ts +34 -0
  26. package/dist/editor/client/hooks/useSocketMessageHandler.js +191 -0
  27. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -0
  28. package/dist/editor/client/hooks/useWorkbox.d.ts +9 -0
  29. package/dist/editor/client/hooks/useWorkbox.js +53 -0
  30. package/dist/editor/client/hooks/useWorkbox.js.map +1 -0
  31. package/dist/editor/client/ui/EditorChrome.d.ts +12 -0
  32. package/dist/editor/client/ui/EditorChrome.js +23 -0
  33. package/dist/editor/client/ui/EditorChrome.js.map +1 -0
  34. package/dist/editor/client/ui/FullscreenControls.d.ts +7 -0
  35. package/dist/editor/client/ui/FullscreenControls.js +21 -0
  36. package/dist/editor/client/ui/FullscreenControls.js.map +1 -0
  37. package/dist/editor/control-center/WebSocketMessages.js.map +1 -1
  38. package/dist/editor/page-editor-chrome/FieldActionIndicator.d.ts +1 -1
  39. package/dist/revision.d.ts +2 -2
  40. package/dist/revision.js +2 -2
  41. package/package.json +1 -1
  42. package/src/editor/Editor.tsx +14 -5
  43. package/src/editor/client/{EditorClient.tsx → EditorShell.tsx} +78 -789
  44. package/src/editor/client/editContext.ts +3 -3
  45. package/src/editor/client/hooks/useEditorUrlSync.ts +83 -0
  46. package/src/editor/client/hooks/useEditorWebSocket.ts +101 -0
  47. package/src/editor/client/hooks/useGlobalEditorEvents.ts +18 -0
  48. package/src/editor/client/hooks/useMediaQuery.ts +21 -0
  49. package/src/editor/client/hooks/useMediaSelector.ts +48 -0
  50. package/src/editor/client/hooks/useQuota.ts +81 -0
  51. package/src/editor/client/hooks/useSocketMessageHandler.ts +285 -0
  52. package/src/editor/client/hooks/useWorkbox.ts +67 -0
  53. package/src/editor/client/ui/EditorChrome.tsx +76 -0
  54. package/src/editor/client/ui/FullscreenControls.tsx +58 -0
  55. package/src/editor/control-center/WebSocketMessages.tsx +1 -2
  56. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +1 -1
  57. package/src/revision.ts +2 -2
  58. package/dist/editor/client/EditorClient.js.map +0 -1
@@ -8,21 +8,19 @@ import { useRouter, useSearchParams, usePathname } from "next/navigation";
8
8
  import { findComponent, getComponentById } from "../componentTreeHelper";
9
9
  import { getOperationsContext } from "./operations";
10
10
  import { handleErrorResult } from "./helpers";
11
- import { executeFieldAction as executeFieldServerAction, connectSocket, getEditHistory, releaseFieldLocks, validateItems, } from "../services/editService";
11
+ import { executeFieldAction as executeFieldServerAction, getEditHistory, releaseFieldLocks, validateItems, } from "../services/editService";
12
12
  import "primeicons/primeicons.css";
13
13
  import "primereact/resources/themes/md-light-indigo/theme.css";
14
14
  import "react-json-view-lite/dist/index.css";
15
15
  import { MediaSelector, } from "../media-selector/MediaSelector";
16
16
  import { getComponentCommands } from "../commands/componentCommands";
17
- import { getLanguagesAndVersions, getWorkbox, } from "../services/contentService";
17
+ import { getLanguagesAndVersions, } from "../services/contentService";
18
18
  import ConfirmationDialog from "../ConfirmationDialog";
19
- import MainLayout from "../MainLayout";
20
- import { getItemDescriptor, useEventListenerExt } from "../utils";
19
+ import { getItemDescriptor } from "../utils";
21
20
  import { EditContextMenu } from "../ContextMenu";
22
21
  import { FieldEditorPopup } from "../FieldEditorPopup";
23
22
  import { EditorFormPopup, } from "../page-viewer/EditorFormPopup";
24
23
  import { post } from "../services/serviceHelper";
25
- import { SidebarView } from "../sidebar/SidebarView";
26
24
  import { PageViewerFrame } from "../page-viewer/PageViewerFrame";
27
25
  import { useItemsRepository } from "./itemsRepository";
28
26
  import { Spinner } from "../ui/Spinner";
@@ -38,10 +36,15 @@ import uuid from "react-uuid";
38
36
  import { flushSync } from "react-dom";
39
37
  import { getSuggestedEdits } from "../services/suggestedEditsService";
40
38
  import { usePageWizard } from "../../page-wizard/usePageWizard";
41
- import { requestQuota } from "../services/aiService";
42
- import { Shrink, Monitor, Smartphone } from "lucide-react";
43
- import { Agents } from "../ai/Agents";
44
- export function EditorClient({ configuration, className, item: loadItemDescriptor, sessionId, userInfo, userPreferences, editorSettings, setUserPreferences, children, }) {
39
+ import { useQuota } from "./hooks/useQuota";
40
+ import { useEditorUrlSync } from "./hooks/useEditorUrlSync";
41
+ import { useMediaQuery } from "./hooks/useMediaQuery";
42
+ import { useGlobalEditorEvents } from "./hooks/useGlobalEditorEvents";
43
+ import { FullscreenControls } from "./ui/FullscreenControls";
44
+ import { EditorChrome } from "./ui/EditorChrome";
45
+ import { useWorkbox } from "./hooks/useWorkbox";
46
+ import { useMediaSelector } from "./hooks/useMediaSelector";
47
+ export function EditorShell({ configuration, className, item: loadItemDescriptor, sessionId, userInfo, userPreferences, editorSettings, setUserPreferences, children, }) {
45
48
  const router = useRouter();
46
49
  const pathname = usePathname();
47
50
  const searchParams = useSearchParams();
@@ -50,10 +53,7 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
50
53
  const [refreshCompletedFlag, setRefreshCompletedFlag] = useState(false);
51
54
  const [isRefreshing, setIsRefreshing] = useState(false);
52
55
  const [dragObject, setDragObject] = useState();
53
- const [mediaResolver, setMediaResolver] = useState();
54
- const [mediaSelectorVisible, setMediaSelectorVisible] = useState(false);
55
- const [mediaSelectorMode, setMediaSelectorMode] = useState("images");
56
- const [selectedMediaIdPath, setSelectedMediaIdPath] = useState("");
56
+ // media selection handled by useMediaSelector hook
57
57
  const [scrollIntoView, setScrollIntoView] = useState();
58
58
  const confirmationDialogRef = useRef(null);
59
59
  const contextMenuRef = useRef(null);
@@ -167,7 +167,7 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
167
167
  const [centerPanelView, setCenterPanelView] = useState();
168
168
  const [timings, setTimings] = useState({});
169
169
  const [revision, setRevision] = useState();
170
- const [workboxItems, setWorkboxItems] = useState([]);
170
+ // moved into useWorkbox
171
171
  const [isTourActive, setIsTourActive] = useState(false);
172
172
  const [mode, setMode] = useState("edit");
173
173
  const [statusMessage, setStatusMessage] = useState("");
@@ -177,7 +177,9 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
177
177
  const [showAgentsPanel, setShowAgentsPanel] = useState(userPreferences.showAgentsPanel ?? false);
178
178
  const [activeEditorTab, setActiveEditorTab] = useState(null);
179
179
  const [showLayoutComponents, setShowLayoutComponents] = useState(userPreferences.showLayoutComponents ?? false);
180
- const [quotaInfo, setQuotaInfo] = useState(null);
180
+ const { quotaInfo, setQuotaInfo, isQuotaExceeded, getQuotaWarningMessage } = useQuota({
181
+ showError: ({ summary, details }) => showErrorToast({ summary, details }),
182
+ });
181
183
  const pageWizard = usePageWizard();
182
184
  const [webSocketMessages, setWebSocketMessages] = useState([]);
183
185
  const [favorites, setFavorites] = useState([]);
@@ -213,7 +215,6 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
213
215
  configuration,
214
216
  });
215
217
  const socketMessageListeners = useRef(new Set());
216
- const socketInstanceRef = useRef(null);
217
218
  const addSocketMessageListener = useCallback((callback) => {
218
219
  socketMessageListeners.current.add(callback);
219
220
  return () => socketMessageListeners.current.delete(callback);
@@ -283,241 +284,7 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
283
284
  setShowLayoutComponents(newValue);
284
285
  setUserPreferences({ showLayoutComponents: newValue });
285
286
  }, [showLayoutComponents, setShowLayoutComponents, setUserPreferences]);
286
- const messageHandler = useCallback(async (event) => {
287
- if (!event.data.startsWith("{"))
288
- return;
289
- const message = JSON.parse(event.data);
290
- // Track all WebSocket messages for debugging/monitoring
291
- try {
292
- const webSocketMessage = {
293
- id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
294
- timestamp: new Date().toISOString(),
295
- type: message.type || "unknown",
296
- payload: message.payload,
297
- rawMessage: JSON.stringify(message, null, 2),
298
- };
299
- setWebSocketMessages((prev) => {
300
- const updated = [webSocketMessage, ...prev];
301
- // Keep only the latest 1000 messages
302
- return updated.slice(0, 1000);
303
- });
304
- }
305
- catch (error) {
306
- console.error("Error tracking WebSocket message:", error);
307
- }
308
- if (message.type === "active-sessions") {
309
- setActiveSessions((prev) => {
310
- // Ensure payload is an array and contains valid session objects
311
- if (!Array.isArray(message.payload)) {
312
- console.warn("❌ Active sessions payload is not an array:", message.payload);
313
- globalThis.__alpacaActiveSessions = prev;
314
- return prev; // keep previous instead of clearing to avoid losing user during HMR blips
315
- }
316
- // Filter out any invalid session objects
317
- const validSessions = message.payload.filter((session) => session &&
318
- typeof session === "object" &&
319
- session.sessionId &&
320
- session.user);
321
- if (validSessions.length !== message.payload.length) {
322
- console.warn("❌ Some sessions were filtered out due to invalid data:", {
323
- original: message.payload.length,
324
- valid: validSessions.length,
325
- });
326
- }
327
- // If server reports empty list, keep previous to avoid transient blanking during HMR
328
- if (validSessions.length === 0 && prev.length > 0) {
329
- console.warn("⚠️ Received empty active sessions; preserving previous list to avoid losing state");
330
- globalThis.__alpacaActiveSessions = prev;
331
- return prev;
332
- }
333
- globalThis.__alpacaActiveSessions = validSessions;
334
- return validSessions;
335
- });
336
- // Detect if the current session is missing from the list and self-heal
337
- try {
338
- const payload = Array.isArray(message.payload)
339
- ? message.payload
340
- : [];
341
- const hasMySession = payload.some((s) => s && s.sessionId === sessionId);
342
- if (!hasMySession) {
343
- console.warn("⚠️ Current session missing from active sessions. Re-sending client-info to recover...");
344
- setTimeout(() => {
345
- sendClientInfo();
346
- }, 300);
347
- }
348
- }
349
- catch (e) {
350
- console.warn("Failed to verify active sessions for current session!", e);
351
- }
352
- }
353
- if (message.type === "item-deleted") {
354
- itemsRepository.onItemsDeleted([
355
- { item: message.payload.item, parentId: message.payload.parentId },
356
- ]);
357
- if (message.payload.item.id === currentItemDescriptor?.id) {
358
- console.log("Load", message.payload.parentId);
359
- loadItem({
360
- id: message.payload.parentId,
361
- language: currentItemDescriptor?.language ?? "en",
362
- version: 0,
363
- });
364
- }
365
- }
366
- if (message.type === "item-changed") {
367
- await itemsRepository.refreshItems([message.payload.item]);
368
- if (message.payload.item.id === currentItemDescriptor?.id)
369
- loadItemVersions();
370
- }
371
- if (message.type === "item-version-added") {
372
- if (currentItemDescriptorRef.current) {
373
- if (currentItemDescriptorRef.current.id === message.payload.item.id)
374
- await loadItemVersions();
375
- }
376
- }
377
- if (message.type === "comment-updated") {
378
- setComments((x) => {
379
- const newComments = [...x];
380
- const index = newComments.findIndex((c) => c.id === message.payload.comment.id);
381
- if (index !== -1)
382
- newComments[index] = message.payload.comment;
383
- else
384
- newComments.push(message.payload.comment);
385
- return newComments;
386
- });
387
- }
388
- if (message.type === "comment-deleted") {
389
- setComments((x) => {
390
- return x.filter((c) => c.id !== message.payload.commentId);
391
- });
392
- }
393
- if (message.type === "suggested-edit-updated") {
394
- setSuggestedEdits((x) => {
395
- const index = x.findIndex((s) => s.id === message.payload.suggestedEdit.id);
396
- if (index !== -1)
397
- x[index] = message.payload.suggestedEdit;
398
- else
399
- x.push(message.payload.suggestedEdit);
400
- return x;
401
- });
402
- }
403
- if (message.type === "suggested-edit-deleted") {
404
- setSuggestedEdits((x) => {
405
- return x.filter((s) => s.id !== message.payload.id);
406
- });
407
- }
408
- if (message.type === "executing-field-action") {
409
- setActiveFieldActions((x) => {
410
- const payload = message.payload;
411
- const fieldId = payload.fieldId;
412
- const item = payload.item;
413
- const status = payload.status;
414
- const msg = payload.message;
415
- const label = payload.label;
416
- // Map backend status to FieldAction state
417
- let state;
418
- switch (status?.toLowerCase()) {
419
- case "completed":
420
- case "success":
421
- state = "success";
422
- break;
423
- case "failed":
424
- case "error":
425
- state = "error";
426
- break;
427
- default:
428
- state = "running";
429
- break;
430
- }
431
- // Check if action already exists
432
- const existingActionIndex = x.findIndex((action) => action.field.fieldId === fieldId &&
433
- action.field.item.id === item.id &&
434
- action.field.item.language === item.language &&
435
- action.field.item.version === item.version);
436
- if (existingActionIndex !== -1) {
437
- // Update existing action
438
- const newActions = [...x];
439
- newActions[existingActionIndex].state = state;
440
- newActions[existingActionIndex].message = msg;
441
- return newActions;
442
- }
443
- else {
444
- // Insert new action
445
- const fieldDescriptor = {
446
- fieldId: fieldId,
447
- item: item,
448
- };
449
- const newAction = {
450
- field: fieldDescriptor,
451
- state,
452
- message: msg,
453
- label: label,
454
- };
455
- // console.log(newAction);
456
- return [...x, newAction];
457
- }
458
- });
459
- }
460
- if (message.type === "update-quota") {
461
- setQuotaInfo(message.payload);
462
- }
463
- if (message.type === "agent-started") {
464
- // Agent started message will be handled by individual components that subscribe
465
- // The payload should contain { agentId: string, agentName: string }
466
- }
467
- if (message.type === "load-item") {
468
- const itemDescriptor = message.payload;
469
- if (itemDescriptor) {
470
- loadItem(itemDescriptor, { skipViewChange: true });
471
- }
472
- }
473
- if (message.type === "edit-operation") {
474
- const op = message.payload;
475
- if (op.type === "edit-field") {
476
- const editFieldOperation = op;
477
- const field = await itemsRepository.getField({
478
- item: {
479
- ...editFieldOperation.mainItem,
480
- id: editFieldOperation.itemId,
481
- },
482
- fieldId: editFieldOperation.fieldId,
483
- });
484
- if (!field ||
485
- (field.type !== "single-line text" &&
486
- field.type !== "multi-line text" &&
487
- field.type !== "rich text")) {
488
- itemsRepository.refreshItems([
489
- {
490
- ...editFieldOperation.mainItem,
491
- id: editFieldOperation.itemId,
492
- },
493
- ]);
494
- requestRefresh("immediate");
495
- }
496
- //TODO: field value changes that require rerender
497
- else
498
- itemsRepository.updateFieldValue({
499
- fieldId: editFieldOperation.fieldId,
500
- item: {
501
- ...editFieldOperation.mainItem,
502
- id: editFieldOperation.itemId,
503
- },
504
- }, editFieldOperation.user ?? { name: "unknown", ai: false }, false, editFieldOperation.undone
505
- ? editFieldOperation.oldValue
506
- : editFieldOperation.value);
507
- }
508
- else {
509
- requestRefresh("immediate");
510
- }
511
- if (op.mainItem &&
512
- op.mainItem.id === currentItemRef.current?.descriptor.id &&
513
- op.mainItem.language ===
514
- currentItemRef.current?.descriptor.language &&
515
- op.mainItem.version === currentItemRef.current?.descriptor.version) {
516
- loadHistory(op.mainItem);
517
- }
518
- }
519
- socketMessageListeners.current.forEach((listener) => listener(message));
520
- }, [currentItemDescriptorRef, addRecentEdit]);
287
+ // messageHandler is defined after loadItem/loadHistory declarations to avoid temporal dead zones
521
288
  const user = useMemo(() => activeSessions.find((x) => x.sessionId === sessionId)?.user ||
522
289
  userInfo.user, [activeSessions, sessionId, userInfo.user]);
523
290
  // Self-heal if our session disappears (e.g., after HMR)
@@ -549,11 +316,9 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
549
316
  else {
550
317
  // Force a reconnect to refresh presence after several failed nudges
551
318
  try {
552
- socketInstanceRef.current?.close(4000, "recover-presence");
553
- }
554
- catch {
555
- // Ignore errors while attempting to recover presence
319
+ globalThis.editorSocket?.close(4000, "recover-presence");
556
320
  }
321
+ catch { }
557
322
  }
558
323
  }, delay);
559
324
  return () => {
@@ -647,25 +412,8 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
647
412
  clearInterval(urlCheckInterval);
648
413
  };
649
414
  }, []);
650
- // Custom hook for responsive design
651
- const useMediaQuery = (query) => {
652
- const [matches, setMatches] = useState(false);
653
- useEffect(() => {
654
- const media = window.matchMedia(query);
655
- const updateMatch = () => setMatches(media.matches);
656
- // Set initial value
657
- updateMatch();
658
- // Listen for changes
659
- media.addEventListener("change", updateMatch);
660
- // Cleanup
661
- return () => {
662
- media.removeEventListener("change", updateMatch);
663
- };
664
- }, [query]);
665
- return matches;
666
- };
667
- // Detect mobile screens (max-width: 768px)
668
415
  const isMobile = useMediaQuery("(max-width: 768px)");
416
+ const media = useMediaSelector();
669
417
  useEffect(() => {
670
418
  // Suppress auto-start tour when `noTour` query parameter is present
671
419
  if (searchParams.get("noTour") !== null) {
@@ -679,117 +427,8 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
679
427
  localStorage.setItem(key, "true");
680
428
  }
681
429
  }, [user]);
682
- useEffect(() => {
683
- let reconnectTimeout = null;
684
- let reconnectAttempts = 0;
685
- const connectWebSocket = () => {
686
- let socket = globalThis.editorSocket;
687
- const needsNewSocket = !socket ||
688
- socket.readyState === WebSocket.CLOSING ||
689
- socket.readyState === WebSocket.CLOSED;
690
- if (needsNewSocket) {
691
- socket = connectSocket(sessionId);
692
- // Connection opened
693
- socket.addEventListener("open", () => {
694
- console.log("Connected!");
695
- reconnectAttempts = 0; // Reset attempts on successful connection
696
- sendClientInfo();
697
- requestQuota();
698
- //TODO: Load clients
699
- });
700
- // Handle connection close
701
- socket.addEventListener("close", (event) => {
702
- // WebSocket connection closed
703
- // Only attempt to reconnect if it wasn't a clean close
704
- if (event.code !== 1000) {
705
- // Start with 1 second, increase exponentially up to 30 seconds
706
- const delay = Math.min(1000 * Math.pow(2, Math.min(reconnectAttempts, 5)), 30000);
707
- // Attempting to reconnect with backoff
708
- reconnectTimeout = setTimeout(() => {
709
- reconnectAttempts++;
710
- connectWebSocket();
711
- }, delay);
712
- }
713
- });
714
- // Handle connection errors
715
- socket.addEventListener("error", (error) => {
716
- console.error("WebSocket error:", error);
717
- });
718
- globalThis.editorSocket = socket;
719
- }
720
- // Always ensure this instance is listening to messages
721
- if (socket) {
722
- socket.addEventListener("message", messageHandler);
723
- socketInstanceRef.current = socket;
724
- // If we attached to an already-open socket, resend client info to refresh presence
725
- if (socket.readyState === WebSocket.OPEN) {
726
- sendClientInfo();
727
- requestQuota();
728
- }
729
- }
730
- };
731
- connectWebSocket();
732
- // Cleanup function
733
- return () => {
734
- if (reconnectTimeout) {
735
- clearTimeout(reconnectTimeout);
736
- }
737
- if (socketInstanceRef.current) {
738
- socketInstanceRef.current.removeEventListener("message", messageHandler);
739
- socketInstanceRef.current = null;
740
- }
741
- };
742
- }, []);
743
- // Handle initial state setup from URL (only on initial load)
744
- useEffect(() => {
745
- if (!isInitialLoad)
746
- return;
747
- const itemid = searchParams.get("itemid");
748
- const wizardId = searchParams.get("wizardid");
749
- const urlView = searchParams.get("view");
750
- // Handle wizard ID from URL first (before view)
751
- if (wizardId) {
752
- setCurrentWizardId(wizardId);
753
- }
754
- if (urlView) {
755
- setViewName(urlView);
756
- }
757
- else if (!itemid) {
758
- setViewName("splash-screen");
759
- }
760
- }, [searchParams, isInitialLoad]);
761
- // Handle initial compare mode from URL (only on initial load)
762
- useEffect(() => {
763
- if (!isInitialLoad)
764
- return;
765
- if (searchParams.has("compare")) {
766
- const compareValue = searchParams.get("compare") === "true";
767
- setCompareMode(compareValue);
768
- }
769
- }, [searchParams, pathname, isInitialLoad]);
770
- // Handle initial item loading from URL and loadItemDescriptor
771
- useEffect(() => {
772
- // Use URL params only on initial load, otherwise respect loadItemDescriptor prop
773
- const itemid = isInitialLoad ? searchParams.get("itemid") : null;
774
- const itemId = cleanId(loadItemDescriptor?.id ?? itemid ?? undefined);
775
- const language = loadItemDescriptor?.language ??
776
- (isInitialLoad
777
- ? (searchParams.get("lang") ?? searchParams.get("language"))
778
- : null);
779
- const version = loadItemDescriptor?.version ??
780
- (isInitialLoad && searchParams.has("version")
781
- ? parseInt(searchParams.get("version"))
782
- : 0);
783
- if (!itemId || !language)
784
- return;
785
- // Skip loading if the item is already loaded with the same parameters
786
- if (currentItemDescriptor?.id === itemId &&
787
- currentItemDescriptor?.language === language &&
788
- (!version || currentItemDescriptor?.version === version)) {
789
- return;
790
- }
791
- loadItem({ id: itemId, language, version });
792
- }, [loadItemDescriptor, searchParams, currentItemDescriptor, isInitialLoad]);
430
+ // WebSocket initialization is performed after messageHandler is defined
431
+ // Defer URL sync until loadItem is defined below
793
432
  // Mark end of initial load phase
794
433
  useEffect(() => {
795
434
  if (isInitialLoad) {
@@ -889,28 +528,7 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
889
528
  if (!isLoading)
890
529
  setInserting(undefined);
891
530
  }, [page, viewName, revision]);
892
- useEffect(() => {
893
- // Handle fullscreen on initial load only
894
- if (isInitialLoad &&
895
- (searchParams.get("fullscreen") || configuration.forceFullscreen)) {
896
- pageViewContext.setFullscreen(true);
897
- }
898
- const handleMessage = (event) => {
899
- if (event.data.action === "refresh") {
900
- requestRefresh("immediate");
901
- }
902
- };
903
- window.addEventListener("message", handleMessage);
904
- return () => {
905
- window.removeEventListener("message", handleMessage);
906
- };
907
- }, [
908
- isInitialLoad,
909
- searchParams,
910
- pathname,
911
- pageViewContext,
912
- configuration.forceFullscreen,
913
- ]);
531
+ // moved below to ensure requestRefresh is declared first
914
532
  const loadHistory = useDebouncedCallback(async (item) => {
915
533
  const result = await getEditHistory(item);
916
534
  if (handleErrorResult(result, ui, state))
@@ -923,6 +541,7 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
923
541
  return;
924
542
  setEditHistory(result.data || []);
925
543
  }, []);
544
+ // defined below after loadItem/loadItemVersions/requestRefresh
926
545
  const requestRefresh = useCallback((mode) => {
927
546
  const refreshTimer = globalThis.editorRefreshTimer;
928
547
  const doRefresh = () => {
@@ -1118,6 +737,20 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
1118
737
  loadHistory,
1119
738
  addToBrowseHistory,
1120
739
  ]);
740
+ useEditorUrlSync({
741
+ isInitialLoad,
742
+ setIsInitialLoad,
743
+ searchParams,
744
+ pathname,
745
+ setCurrentWizardId,
746
+ setViewName,
747
+ setCompareMode,
748
+ loadItem,
749
+ currentItemDescriptor,
750
+ loadItemDescriptor,
751
+ routerPush: (url) => router.push(url, { scroll: false }),
752
+ });
753
+ // initialized below after messageHandler
1121
754
  useEffect(() => {
1122
755
  if (pageViewContext.fullscreen &&
1123
756
  !searchParams.get("fullscreen") &&
@@ -1222,78 +855,12 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
1222
855
  confirmationDialogRef,
1223
856
  onOperationExecuted,
1224
857
  }), [showErrorToast, confirmationDialogRef, onOperationExecuted]);
1225
- const selectMedia = useCallback(({ selectedIdPath, mode, }) => {
1226
- setSelectedMediaIdPath(selectedIdPath);
1227
- setMediaSelectorVisible(true);
1228
- if (mode)
1229
- setMediaSelectorMode(mode);
1230
- return new Promise((resolve) => {
1231
- setMediaResolver(() => resolve);
1232
- });
1233
- }, []);
1234
- const onMediaSelect = useCallback((mediaUrl) => {
1235
- mediaResolver?.(mediaUrl);
1236
- setMediaSelectorVisible(false);
1237
- setMediaResolver(undefined);
1238
- }, [mediaResolver]);
1239
- useEffect(() => {
1240
- if (!workboxItems || workboxItems.length === 0)
1241
- return;
1242
- const itemsToValidate = workboxItems.map((x) => x.item);
1243
- validate(itemsToValidate);
1244
- }, [workboxItems]);
1245
- async function loadWorkbox(items) {
1246
- if (!items.length) {
1247
- setWorkboxItems([]);
1248
- return;
1249
- }
1250
- const workbox = await getWorkbox(items.map((x) => getItemDescriptor(x)));
1251
- const workboxItems = workbox.data || [];
1252
- const sortedWorkboxItems = workboxItems.sort((a, b) => {
1253
- if (a.isPublished === b.isPublished)
1254
- return ((b.workflowCommands?.length || 0) - (a.workflowCommands?.length || 0));
1255
- return !a.isPublished || !a.isPublishable ? -1 : 1;
1256
- });
1257
- setWorkboxItems(sortedWorkboxItems);
1258
- }
1259
- const loadWorkboxDebounced = useDebouncedCallback((items) => loadWorkbox(items), 5000);
1260
- useEffect(() => {
1261
- const items = [];
1262
- if (editContext.contentEditorItem) {
1263
- items.push(editContext.contentEditorItem.descriptor);
1264
- }
1265
- if (editContext.page) {
1266
- collectAllItems(editContext.page.rootComponent, items);
1267
- }
1268
- loadWorkboxDebounced(items.filter((x) => x));
1269
- }, [page, contentEditorItem]);
1270
- function collectAllItems(component, items) {
1271
- component.placeholders.forEach((x) => {
1272
- x.components.forEach((y) => {
1273
- if (y.isShared && y.datasourceItem) {
1274
- items.push(y.datasourceItem);
1275
- }
1276
- //TODO: Add picture fields
1277
- // y.datasourceItem?.fields.forEach((z) => {
1278
- // if (z.type === "picture") {
1279
- // const picture = z.value as PictureValue;
1280
- // if (picture.variants) {
1281
- // picture.variants.forEach((v) => {
1282
- // if (v.mediaId) {
1283
- // items.push({
1284
- // id: v.mediaId,
1285
- // language: y.datasourceItem!.descriptor.language,
1286
- // version: 0,
1287
- // });
1288
- // }
1289
- // });
1290
- // }
1291
- // }
1292
- // });
1293
- collectAllItems(y, items);
1294
- });
1295
- });
1296
- }
858
+ // moved to useMediaSelector
859
+ const { workboxItems } = useWorkbox({
860
+ page,
861
+ contentEditorItem,
862
+ validate,
863
+ });
1297
864
  const switchView = (viewName, options) => {
1298
865
  async function switchView() {
1299
866
  if (currentView?.beforeClose && !options?.skipConfirmation) {
@@ -1375,12 +942,10 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
1375
942
  showErrorToast,
1376
943
  executeCommand,
1377
944
  });
1378
- if (typeof window !== "undefined")
1379
- useEventListenerExt("keydown", handleKeyDown, window, true);
1380
- if (typeof window !== "undefined")
1381
- useEventListenerExt("click", () => {
1382
- contextMenuRef.current?.close({});
1383
- }, window, true);
945
+ useGlobalEditorEvents({
946
+ onKeyDown: handleKeyDown,
947
+ onWindowClick: () => contextMenuRef.current?.close({}),
948
+ });
1384
949
  useEffect(() => {
1385
950
  if (mode === "suggestions") {
1386
951
  setShowSuggestedEdits(true);
@@ -1406,57 +971,7 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
1406
971
  router.push(newUrl, { scroll: false });
1407
972
  }, [router]);
1408
973
  // Quota checking functions
1409
- const isQuotaExceeded = useCallback(() => {
1410
- if (!quotaInfo)
1411
- return false;
1412
- const { usage, limits } = quotaInfo;
1413
- // Check absolute limits
1414
- if (limits.totalTokens > 0 && usage.totalTokens >= limits.totalTokens)
1415
- return true;
1416
- if (limits.totalImages > 0 && usage.totalImages >= limits.totalImages)
1417
- return true;
1418
- // For now, we're only checking absolute limits as daily/monthly would require server-side logic
1419
- // You can extend this to check daily/monthly limits if the server provides that information
1420
- return false;
1421
- }, [quotaInfo]);
1422
- const getQuotaWarningMessage = useCallback(() => {
1423
- if (!quotaInfo)
1424
- return null;
1425
- const { usage, limits } = quotaInfo;
1426
- const warnings = [];
1427
- // Check tokens
1428
- if (limits.totalTokens > 0) {
1429
- const tokenPercentage = (usage.totalTokens / limits.totalTokens) * 100;
1430
- if (tokenPercentage >= 100) {
1431
- warnings.push(`Token limit exceeded (${usage.totalTokens}/${limits.totalTokens})`);
1432
- }
1433
- else if (tokenPercentage >= 90) {
1434
- warnings.push(`Token usage high: ${Math.round(tokenPercentage)}% (${usage.totalTokens}/${limits.totalTokens})`);
1435
- }
1436
- }
1437
- // Check images
1438
- if (limits.totalImages > 0) {
1439
- const imagePercentage = (usage.totalImages / limits.totalImages) * 100;
1440
- if (imagePercentage >= 100) {
1441
- warnings.push(`Image limit exceeded (${usage.totalImages}/${limits.totalImages})`);
1442
- }
1443
- else if (imagePercentage >= 90) {
1444
- warnings.push(`Image usage high: ${Math.round(imagePercentage)}% (${usage.totalImages}/${limits.totalImages})`);
1445
- }
1446
- }
1447
- return warnings.length > 0 ? warnings.join(", ") : null;
1448
- }, [quotaInfo]);
1449
- // Show warning when quota is exceeded
1450
- useEffect(() => {
1451
- const warningMessage = getQuotaWarningMessage();
1452
- if (warningMessage) {
1453
- const isExceeded = isQuotaExceeded();
1454
- showErrorToast({
1455
- summary: isExceeded ? "AI Quota Exceeded" : "AI Quota Warning",
1456
- details: warningMessage,
1457
- });
1458
- }
1459
- }, [quotaInfo, getQuotaWarningMessage, isQuotaExceeded, showErrorToast]);
974
+ // moved into hook
1460
975
  // Calculate visible views separately to avoid circular dependency
1461
976
  const visibleViews = useMemo(() => {
1462
977
  const allViews = configuration.editor.views
@@ -1519,7 +1034,7 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
1519
1034
  const oldUrl = `${pathname}?${searchParams.toString()}`;
1520
1035
  router.push(newUrl, { scroll: false });
1521
1036
  },
1522
- selectMedia,
1037
+ selectMedia: media.selectMedia,
1523
1038
  showToast: (message) => {
1524
1039
  toast(message);
1525
1040
  },
@@ -1843,7 +1358,6 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
1843
1358
  searchParams,
1844
1359
  pathname,
1845
1360
  router,
1846
- selectMedia,
1847
1361
  scrollIntoView,
1848
1362
  focusedField,
1849
1363
  renderedFields,
@@ -2170,42 +1684,11 @@ export function EditorClient({ configuration, className, item: loadItemDescripto
2170
1684
  }, [currentItemDescriptor]);
2171
1685
  if (!currentView)
2172
1686
  return null;
2173
- const editorUi = pageViewContext.fullscreen ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "fixed inset-0 flex", children: [_jsx(PageViewerFrame, { compareView: compareMode, pageViewContext: pageViewContext }), _jsxs("div", { className: "fixed top-4 right-4 z-[9999] flex gap-2", children: [_jsx("button", { onClick: () => {
2174
- const currentDevice = pageViewContext.device;
2175
- if (currentDevice === "desktop") {
2176
- // Switch to mobile (first mobile device from configuration)
2177
- const firstMobileDevice = configuration.devices[0];
2178
- if (firstMobileDevice) {
2179
- pageViewContext.setDevice(firstMobileDevice.name);
2180
- }
2181
- }
2182
- else {
2183
- // Switch to desktop
2184
- pageViewContext.setDevice("desktop");
2185
- }
2186
- }, className: "flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-black/20 text-white backdrop-blur-sm transition-colors duration-200 hover:bg-black/40", "aria-label": pageViewContext.device === "desktop"
2187
- ? "Switch to mobile view"
2188
- : "Switch to desktop view", title: pageViewContext.device === "desktop"
2189
- ? "Switch to mobile view"
2190
- : "Switch to desktop view", "data-testid": "fullscreen-device-toggle", children: pageViewContext.device === "desktop" ? (_jsx(Smartphone, { className: "h-5 w-5" })) : (_jsx(Monitor, { className: "h-5 w-5" })) }), !configuration.forceFullscreen && (_jsx("button", { onClick: () => pageViewContext.setFullscreen(false), className: "flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-black/20 text-white backdrop-blur-sm transition-colors duration-200 hover:bg-black/40", "aria-label": "Exit fullscreen", title: "Return to normal view", "data-testid": "fullscreen-exit-button", children: _jsx(Shrink, { className: "h-5 w-5" }) }))] })] }), _jsx(EditorFormPopup, { ref: editorFormPopupRef, pageViewContext: pageViewContext }), showFullscreenHint && !configuration.forceFullscreen && (_jsx("div", { className: "fixed inset-0", onMouseMoveCapture: () => {
1687
+ const editorUi = pageViewContext.fullscreen ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "fixed inset-0 flex", children: [_jsx(PageViewerFrame, { compareView: compareMode, pageViewContext: pageViewContext }), _jsx(FullscreenControls, { device: pageViewContext.device, setDevice: (d) => pageViewContext.setDevice(d), canExit: !configuration.forceFullscreen, onExit: () => pageViewContext.setFullscreen(false), firstMobileDeviceName: configuration.devices[0]?.name })] }), _jsx(EditorFormPopup, { ref: editorFormPopupRef, pageViewContext: pageViewContext }), showFullscreenHint && !configuration.forceFullscreen && (_jsx("div", { className: "fixed inset-0", onMouseMoveCapture: () => {
2191
1688
  setTimeout(() => {
2192
1689
  setShowFullscreenHint(false);
2193
1690
  }, 600);
2194
- }, "data-testid": "fullscreen-hint-overlay", children: _jsx("div", { className: "fixed top-3 left-1/2 -translate-x-1/2 transform rounded-sm bg-gray-200 p-12", children: "Press Ctrl + F11 to exit fullscreen mode" }) }))] })) : (_jsxs(_Fragment, { children: [_jsx(MainLayout, { className: className, view: currentView, centerPanelView: centerPanelView, rightSidebar: currentView.rightSidebar &&
2195
- editContext.page &&
2196
- showComponentNavigator && (_jsx(SidebarView, { sidebar: currentView.rightSidebar, editContext: editContext, active: true, detached: true, onClose: () => handleSetShowComponentNavigator(false) })), rightSidebarTitle: currentView.rightSidebar?.title, farRightSidebar: showAgentsPanel &&
2197
- !["splash-screen", "open-page", "new-page"].includes(viewName) && (_jsx(SidebarView, { sidebar: {
2198
- title: "Agents",
2199
- panels: [
2200
- {
2201
- name: "agents",
2202
- title: "Agents",
2203
- content: _jsx(Agents, {}),
2204
- initialSize: 70,
2205
- noOverflow: true,
2206
- },
2207
- ],
2208
- }, editContext: editContext, active: true, detached: true, paddingRight: true, onClose: () => handleSetShowAgentsPanel(false) })), farRightSidebarTitle: "AGENTS" }), isTourActive && _jsx(Tour, { tourStopCallback: () => setIsTourActive(false) })] }));
2209
- return (_jsx("div", { className: `editor h-full`, children: _jsx(OperationsContextProvider, { value: operationsContext.context, children: _jsx(FieldsEditContextProvider, { value: fieldsEditContext, children: _jsxs(EditContextProvider, { value: editContext, children: [editContext.isRefreshing && (_jsx("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", children: _jsx(Spinner, {}) })), children || editorUi, dialog, _jsx(Toaster, { position: "top-center" }), " ", _jsx(ConfirmationDialog, { ref: confirmationDialogRef }), _jsx(EditContextMenu, { ref: contextMenuRef }), mediaSelectorVisible && (_jsx(MediaSelector, { language: editContext.currentItemDescriptor.language, visible: mediaSelectorVisible, onHide: () => setMediaSelectorVisible(false), onMediaSelected: onMediaSelect, selectedIdPath: selectedMediaIdPath, mode: mediaSelectorMode })), _jsx(FieldEditorPopup, { ref: fieldEditorPopupRef }), _jsx(EditorFormPopup, { ref: editorFormPopupRef, pageViewContext: pageViewContext })] }) }) }) }));
1691
+ }, "data-testid": "fullscreen-hint-overlay", children: _jsx("div", { className: "fixed top-3 left-1/2 -translate-x-1/2 transform rounded-sm bg-gray-200 p-12", children: "Press Ctrl + F11 to exit fullscreen mode" }) }))] })) : (_jsxs(_Fragment, { children: [_jsx(EditorChrome, { className: className, currentView: currentView, centerPanelView: centerPanelView, editContext: editContext, showComponentNavigator: showComponentNavigator, handleSetShowComponentNavigator: handleSetShowComponentNavigator, showAgentsPanel: showAgentsPanel, handleSetShowAgentsPanel: handleSetShowAgentsPanel, viewName: viewName }), isTourActive && _jsx(Tour, { tourStopCallback: () => setIsTourActive(false) })] }));
1692
+ return (_jsx("div", { className: `editor h-full`, children: _jsx(OperationsContextProvider, { value: operationsContext.context, children: _jsx(FieldsEditContextProvider, { value: fieldsEditContext, children: _jsxs(EditContextProvider, { value: editContext, children: [editContext.isRefreshing && (_jsx("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", children: _jsx(Spinner, {}) })), children || editorUi, dialog, _jsx(Toaster, { position: "top-center" }), " ", _jsx(ConfirmationDialog, { ref: confirmationDialogRef }), _jsx(EditContextMenu, { ref: contextMenuRef }), media.mediaSelectorVisible && (_jsx(MediaSelector, { language: editContext.currentItemDescriptor.language, visible: media.mediaSelectorVisible, onHide: () => media.setMediaSelectorVisible(false), onMediaSelected: media.onMediaSelect, selectedIdPath: media.selectedMediaIdPath, mode: media.mediaSelectorMode })), _jsx(FieldEditorPopup, { ref: fieldEditorPopupRef }), _jsx(EditorFormPopup, { ref: editorFormPopupRef, pageViewContext: pageViewContext })] }) }) }) }));
2210
1693
  }
2211
- //# sourceMappingURL=EditorClient.js.map
1694
+ //# sourceMappingURL=EditorShell.js.map