@alpaca-editor/core 1.0.4073 → 1.0.4075

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 (105) hide show
  1. package/dist/components/ui/context-menu.js +1 -1
  2. package/dist/components/ui/context-menu.js.map +1 -1
  3. package/dist/config/types.d.ts +1 -0
  4. package/dist/editor/ContextMenu.js +25 -14
  5. package/dist/editor/ContextMenu.js.map +1 -1
  6. package/dist/editor/Editor.js +3 -3
  7. package/dist/editor/Editor.js.map +1 -1
  8. package/dist/editor/FieldListField.js +1 -1
  9. package/dist/editor/FieldListField.js.map +1 -1
  10. package/dist/editor/ai/Agents.js +7 -0
  11. package/dist/editor/ai/Agents.js.map +1 -1
  12. package/dist/editor/client/{EditorClient.d.ts → EditorShell.d.ts} +1 -19
  13. package/dist/editor/client/{EditorClient.js → EditorShell.js} +100 -575
  14. package/dist/editor/client/EditorShell.js.map +1 -0
  15. package/dist/editor/client/editContext.d.ts +2 -1
  16. package/dist/editor/client/hooks/useEditorUrlSync.d.ts +18 -0
  17. package/dist/editor/client/hooks/useEditorUrlSync.js +56 -0
  18. package/dist/editor/client/hooks/useEditorUrlSync.js.map +1 -0
  19. package/dist/editor/client/hooks/useEditorWebSocket.d.ts +11 -0
  20. package/dist/editor/client/hooks/useEditorWebSocket.js +86 -0
  21. package/dist/editor/client/hooks/useEditorWebSocket.js.map +1 -0
  22. package/dist/editor/client/hooks/useGlobalEditorEvents.d.ts +4 -0
  23. package/dist/editor/client/hooks/useGlobalEditorEvents.js +15 -0
  24. package/dist/editor/client/hooks/useGlobalEditorEvents.js.map +1 -0
  25. package/dist/editor/client/hooks/useMediaQuery.d.ts +1 -0
  26. package/dist/editor/client/hooks/useMediaQuery.js +19 -0
  27. package/dist/editor/client/hooks/useMediaQuery.js.map +1 -0
  28. package/dist/editor/client/hooks/useMediaSelector.d.ts +12 -0
  29. package/dist/editor/client/hooks/useMediaSelector.js +30 -0
  30. package/dist/editor/client/hooks/useMediaSelector.js.map +1 -0
  31. package/dist/editor/client/hooks/useQuota.d.ts +29 -0
  32. package/dist/editor/client/hooks/useQuota.js +53 -0
  33. package/dist/editor/client/hooks/useQuota.js.map +1 -0
  34. package/dist/editor/client/hooks/useSocketMessageHandler.d.ts +34 -0
  35. package/dist/editor/client/hooks/useSocketMessageHandler.js +191 -0
  36. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -0
  37. package/dist/editor/client/hooks/useWorkbox.d.ts +9 -0
  38. package/dist/editor/client/hooks/useWorkbox.js +53 -0
  39. package/dist/editor/client/hooks/useWorkbox.js.map +1 -0
  40. package/dist/editor/client/ui/EditorChrome.d.ts +12 -0
  41. package/dist/editor/client/ui/EditorChrome.js +23 -0
  42. package/dist/editor/client/ui/EditorChrome.js.map +1 -0
  43. package/dist/editor/client/ui/FullscreenControls.d.ts +7 -0
  44. package/dist/editor/client/ui/FullscreenControls.js +21 -0
  45. package/dist/editor/client/ui/FullscreenControls.js.map +1 -0
  46. package/dist/editor/commands/componentCommands.js +6 -1
  47. package/dist/editor/commands/componentCommands.js.map +1 -1
  48. package/dist/editor/control-center/Info.js +1 -1
  49. package/dist/editor/control-center/Info.js.map +1 -1
  50. package/dist/editor/control-center/WebSocketMessages.js.map +1 -1
  51. package/dist/editor/menubar/ActiveUsers.js +3 -3
  52. package/dist/editor/menubar/ActiveUsers.js.map +1 -1
  53. package/dist/editor/menubar/User.js +4 -2
  54. package/dist/editor/menubar/User.js.map +1 -1
  55. package/dist/editor/page-editor-chrome/FieldActionIndicator.d.ts +1 -1
  56. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +5 -7
  57. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  58. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js +11 -3
  59. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js.map +1 -1
  60. package/dist/editor/reviews/CommentPopover.d.ts +9 -2
  61. package/dist/editor/reviews/CommentPopover.js +12 -10
  62. package/dist/editor/reviews/CommentPopover.js.map +1 -1
  63. package/dist/editor/reviews/PreviewInfo.js +1 -1
  64. package/dist/editor/reviews/PreviewInfo.js.map +1 -1
  65. package/dist/editor/reviews/Reviews.js +2 -2
  66. package/dist/editor/reviews/Reviews.js.map +1 -1
  67. package/dist/editor/ui/Icons.js +1 -1
  68. package/dist/editor/ui/Icons.js.map +1 -1
  69. package/dist/revision.d.ts +2 -2
  70. package/dist/revision.js +2 -2
  71. package/dist/types.d.ts +1 -1
  72. package/package.json +1 -1
  73. package/src/components/ui/context-menu.tsx +1 -1
  74. package/src/config/types.ts +1 -0
  75. package/src/editor/ContextMenu.tsx +54 -34
  76. package/src/editor/Editor.tsx +14 -5
  77. package/src/editor/FieldListField.tsx +1 -1
  78. package/src/editor/ai/Agents.tsx +6 -0
  79. package/src/editor/client/{EditorClient.tsx → EditorShell.tsx} +124 -792
  80. package/src/editor/client/editContext.ts +3 -3
  81. package/src/editor/client/hooks/useEditorUrlSync.ts +83 -0
  82. package/src/editor/client/hooks/useEditorWebSocket.ts +122 -0
  83. package/src/editor/client/hooks/useGlobalEditorEvents.ts +18 -0
  84. package/src/editor/client/hooks/useMediaQuery.ts +21 -0
  85. package/src/editor/client/hooks/useMediaSelector.ts +48 -0
  86. package/src/editor/client/hooks/useQuota.ts +81 -0
  87. package/src/editor/client/hooks/useSocketMessageHandler.ts +285 -0
  88. package/src/editor/client/hooks/useWorkbox.ts +67 -0
  89. package/src/editor/client/ui/EditorChrome.tsx +76 -0
  90. package/src/editor/client/ui/FullscreenControls.tsx +58 -0
  91. package/src/editor/commands/componentCommands.tsx +6 -1
  92. package/src/editor/control-center/Info.tsx +9 -7
  93. package/src/editor/control-center/WebSocketMessages.tsx +1 -2
  94. package/src/editor/menubar/ActiveUsers.tsx +7 -5
  95. package/src/editor/menubar/User.tsx +5 -2
  96. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +1 -1
  97. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +9 -8
  98. package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +14 -3
  99. package/src/editor/reviews/CommentPopover.tsx +20 -9
  100. package/src/editor/reviews/PreviewInfo.tsx +6 -6
  101. package/src/editor/reviews/Reviews.tsx +8 -2
  102. package/src/editor/ui/Icons.tsx +4 -4
  103. package/src/revision.ts +2 -2
  104. package/src/types.ts +1 -1
  105. package/dist/editor/client/EditorClient.js.map +0 -1
@@ -40,6 +40,8 @@ import {
40
40
  releaseFieldLocks,
41
41
  validateItems,
42
42
  } from "../services/editService";
43
+ import { useEditorWebSocket } from "./hooks/useEditorWebSocket";
44
+ import { useSocketMessageHandler } from "./hooks/useSocketMessageHandler";
43
45
 
44
46
  import "primeicons/primeicons.css";
45
47
  import "primereact/resources/themes/md-light-indigo/theme.css";
@@ -127,9 +129,15 @@ import { flushSync } from "react-dom";
127
129
  import { getSuggestedEdits } from "../services/suggestedEditsService";
128
130
  import { usePageWizard } from "../../page-wizard/usePageWizard";
129
131
  import { requestQuota } from "../services/aiService";
132
+ import { useQuota } from "./hooks/useQuota";
133
+ import { useEditorUrlSync } from "./hooks/useEditorUrlSync";
130
134
 
131
- import { Shrink, Monitor, Smartphone } from "lucide-react";
132
- import { Agents } from "../ai/Agents";
135
+ import { useMediaQuery } from "./hooks/useMediaQuery";
136
+ import { useGlobalEditorEvents } from "./hooks/useGlobalEditorEvents";
137
+ import { FullscreenControls } from "./ui/FullscreenControls";
138
+ import { EditorChrome } from "./ui/EditorChrome";
139
+ import { useWorkbox } from "./hooks/useWorkbox";
140
+ import { useMediaSelector } from "./hooks/useMediaSelector";
133
141
 
134
142
  export type FieldAction = {
135
143
  field: FieldDescriptor;
@@ -143,21 +151,7 @@ export type InsertingState = {
143
151
  positionElement: Element;
144
152
  positionAnchor: "left" | "right" | "top" | "bottom";
145
153
  };
146
- export type QuotaUsage = {
147
- totalTokens: number;
148
- totalImages: number;
149
- dailyTokens: number;
150
- dailyImages: number;
151
- };
152
- export type QuotaLimits = {
153
- totalTokens: number;
154
- dailyTokens: number;
155
- monthlyTokens: number;
156
- totalImages: number;
157
- dailyImages: number;
158
- monthlyImages: number;
159
- };
160
- export type QuotaInfo = { usage: QuotaUsage; limits: QuotaLimits };
154
+ // moved to hooks/useQuota
161
155
 
162
156
  export type WebSocketMessage = {
163
157
  id: string;
@@ -167,7 +161,7 @@ export type WebSocketMessage = {
167
161
  rawMessage: string;
168
162
  };
169
163
 
170
- export function EditorClient({
164
+ export function EditorShell({
171
165
  configuration,
172
166
  className,
173
167
  item: loadItemDescriptor,
@@ -199,12 +193,7 @@ export function EditorClient({
199
193
 
200
194
  const [isRefreshing, setIsRefreshing] = useState(false);
201
195
  const [dragObject, setDragObject] = useState<DragObject>();
202
- const [mediaResolver, setMediaResolver] = useState<(value: string) => void>();
203
- const [mediaSelectorVisible, setMediaSelectorVisible] = useState(false);
204
- const [mediaSelectorMode, setMediaSelectorMode] = useState<
205
- "images" | "video"
206
- >("images");
207
- const [selectedMediaIdPath, setSelectedMediaIdPath] = useState<string>("");
196
+ // media selection handled by useMediaSelector hook
208
197
  const [scrollIntoView, setScrollIntoView] = useState<string>();
209
198
 
210
199
  const confirmationDialogRef = useRef<ConfirmationDialogHandle>(null);
@@ -215,6 +204,7 @@ export function EditorClient({
215
204
  const [contentEditorItem, setContentEditorItem] = useState<FullItem>();
216
205
 
217
206
  const [focusedField, setFocusedField] = useState<FieldDescriptor>();
207
+ const focusedFieldRef = useRef<FieldDescriptor | undefined>(undefined);
218
208
  const [selectedRange, setSelectedRange] = useState<SelectionRange>();
219
209
 
220
210
  const [validating, setValidating] = useState(false);
@@ -374,7 +364,7 @@ export function EditorClient({
374
364
 
375
365
  const [revision, setRevision] = useState<string>();
376
366
 
377
- const [workboxItems, setWorkboxItems] = useState<WorkboxItem[]>([]);
367
+ // moved into useWorkbox
378
368
  const [isTourActive, setIsTourActive] = useState(false);
379
369
 
380
370
  const [mode, setMode] = useState<EditorMode>("edit");
@@ -393,7 +383,10 @@ export function EditorClient({
393
383
  const [showLayoutComponents, setShowLayoutComponents] = useState(
394
384
  userPreferences.showLayoutComponents ?? false,
395
385
  );
396
- const [quotaInfo, setQuotaInfo] = useState<QuotaInfo | null>(null);
386
+ const { quotaInfo, setQuotaInfo, isQuotaExceeded, getQuotaWarningMessage } =
387
+ useQuota({
388
+ showError: ({ summary, details }) => showErrorToast({ summary, details }),
389
+ });
397
390
  const pageWizard = usePageWizard();
398
391
 
399
392
  const [webSocketMessages, setWebSocketMessages] = useState<
@@ -434,6 +427,10 @@ export function EditorClient({
434
427
  }
435
428
  }, [selection]);
436
429
 
430
+ useEffect(() => {
431
+ focusedFieldRef.current = focusedField;
432
+ }, [focusedField]);
433
+
437
434
  const itemsRepository = useItemsRepository(setModifiedFields, addRecentEdit);
438
435
 
439
436
  const pageViewContext = usePageViewContext({
@@ -443,7 +440,6 @@ export function EditorClient({
443
440
  });
444
441
 
445
442
  const socketMessageListeners = useRef<Set<(data: any) => void>>(new Set());
446
- const socketInstanceRef = useRef<WebSocket | null>(null);
447
443
 
448
444
  const addSocketMessageListener = useCallback(
449
445
  (callback: (message: { type: string; payload: any }) => void) => {
@@ -469,6 +465,13 @@ export function EditorClient({
469
465
  setSelectedForInsertion("");
470
466
  }, [selection]);
471
467
 
468
+ // Clear any selected insertion template when insert mode is disabled
469
+ useEffect(() => {
470
+ if (!insertMode) {
471
+ setSelectedForInsertion("");
472
+ }
473
+ }, [insertMode]);
474
+
472
475
  useEffect(() => {
473
476
  if (focusedField?.fieldId !== selectedRange?.fieldId) {
474
477
  setSelectedRange(undefined);
@@ -543,298 +546,7 @@ export function EditorClient({
543
546
  [showLayoutComponents, setShowLayoutComponents, setUserPreferences],
544
547
  );
545
548
 
546
- const messageHandler = useCallback(
547
- async (event: any) => {
548
- if (!event.data.startsWith("{")) return;
549
- const message = JSON.parse(event.data);
550
-
551
- // Track all WebSocket messages for debugging/monitoring
552
- try {
553
- const webSocketMessage: WebSocketMessage = {
554
- id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
555
- timestamp: new Date().toISOString(),
556
- type: message.type || "unknown",
557
- payload: message.payload,
558
- rawMessage: JSON.stringify(message, null, 2),
559
- };
560
-
561
- setWebSocketMessages((prev) => {
562
- const updated = [webSocketMessage, ...prev];
563
- // Keep only the latest 1000 messages
564
- return updated.slice(0, 1000);
565
- });
566
- } catch (error) {
567
- console.error("Error tracking WebSocket message:", error);
568
- }
569
-
570
- if (message.type === "active-sessions") {
571
- setActiveSessions((prev) => {
572
- // Ensure payload is an array and contains valid session objects
573
- if (!Array.isArray(message.payload)) {
574
- console.warn(
575
- "❌ Active sessions payload is not an array:",
576
- message.payload,
577
- );
578
- (globalThis as any).__alpacaActiveSessions = prev;
579
- return prev; // keep previous instead of clearing to avoid losing user during HMR blips
580
- }
581
-
582
- // Filter out any invalid session objects
583
- const validSessions = message.payload.filter(
584
- (session: any) =>
585
- session &&
586
- typeof session === "object" &&
587
- session.sessionId &&
588
- session.user,
589
- );
590
-
591
- if (validSessions.length !== message.payload.length) {
592
- console.warn(
593
- "❌ Some sessions were filtered out due to invalid data:",
594
- {
595
- original: message.payload.length,
596
- valid: validSessions.length,
597
- },
598
- );
599
- }
600
-
601
- // If server reports empty list, keep previous to avoid transient blanking during HMR
602
- if (validSessions.length === 0 && prev.length > 0) {
603
- console.warn(
604
- "⚠️ Received empty active sessions; preserving previous list to avoid losing state",
605
- );
606
- (globalThis as any).__alpacaActiveSessions = prev;
607
- return prev;
608
- }
609
-
610
- (globalThis as any).__alpacaActiveSessions = validSessions;
611
- return validSessions;
612
- });
613
-
614
- // Detect if the current session is missing from the list and self-heal
615
- try {
616
- const payload = Array.isArray(message.payload)
617
- ? (message.payload as any[])
618
- : [];
619
- const hasMySession = payload.some(
620
- (s) => s && s.sessionId === sessionId,
621
- );
622
- if (!hasMySession) {
623
- console.warn(
624
- "⚠️ Current session missing from active sessions. Re-sending client-info to recover...",
625
- );
626
- setTimeout(() => {
627
- sendClientInfo();
628
- }, 300);
629
- }
630
- } catch (e) {
631
- console.warn(
632
- "Failed to verify active sessions for current session!",
633
- e,
634
- );
635
- }
636
- }
637
-
638
- if (message.type === "item-deleted") {
639
- itemsRepository.onItemsDeleted([
640
- { item: message.payload.item, parentId: message.payload.parentId },
641
- ]);
642
- if (message.payload.item.id === currentItemDescriptor?.id) {
643
- console.log("Load", message.payload.parentId);
644
- loadItem({
645
- id: message.payload.parentId,
646
- language: currentItemDescriptor?.language ?? "en",
647
- version: 0,
648
- });
649
- }
650
- }
651
-
652
- if (message.type === "item-changed") {
653
- await itemsRepository.refreshItems([message.payload.item]);
654
- if (message.payload.item.id === currentItemDescriptor?.id)
655
- loadItemVersions();
656
- }
657
-
658
- if (message.type === "item-version-added") {
659
- if (currentItemDescriptorRef.current) {
660
- if (currentItemDescriptorRef.current.id === message.payload.item.id)
661
- await loadItemVersions();
662
- }
663
- }
664
-
665
- if (message.type === "comment-updated") {
666
- setComments((x) => {
667
- const newComments = [...x];
668
- const index = newComments.findIndex(
669
- (c) => c.id === message.payload.comment.id,
670
- );
671
- if (index !== -1) newComments[index] = message.payload.comment;
672
- else newComments.push(message.payload.comment);
673
- return newComments;
674
- });
675
- }
676
-
677
- if (message.type === "comment-deleted") {
678
- setComments((x) => {
679
- return x.filter((c) => c.id !== message.payload.commentId);
680
- });
681
- }
682
-
683
- if (message.type === "suggested-edit-updated") {
684
- setSuggestedEdits((x) => {
685
- const index = x.findIndex(
686
- (s) => s.id === message.payload.suggestedEdit.id,
687
- );
688
- if (index !== -1) x[index] = message.payload.suggestedEdit;
689
- else x.push(message.payload.suggestedEdit);
690
- return x;
691
- });
692
- }
693
-
694
- if (message.type === "suggested-edit-deleted") {
695
- setSuggestedEdits((x) => {
696
- return x.filter((s) => s.id !== message.payload.id);
697
- });
698
- }
699
-
700
- if (message.type === "executing-field-action") {
701
- setActiveFieldActions((x) => {
702
- const payload = message.payload;
703
- const fieldId = payload.fieldId;
704
- const item = payload.item;
705
- const status = payload.status;
706
- const msg = payload.message;
707
- const label = payload.label;
708
-
709
- // Map backend status to FieldAction state
710
- let state: "running" | "success" | "error";
711
- switch (status?.toLowerCase()) {
712
- case "completed":
713
- case "success":
714
- state = "success";
715
- break;
716
- case "failed":
717
- case "error":
718
- state = "error";
719
- break;
720
- default:
721
- state = "running";
722
- break;
723
- }
724
-
725
- // Check if action already exists
726
- const existingActionIndex = x.findIndex(
727
- (action) =>
728
- action.field.fieldId === fieldId &&
729
- action.field.item.id === item.id &&
730
- action.field.item.language === item.language &&
731
- action.field.item.version === item.version,
732
- );
733
-
734
- if (existingActionIndex !== -1) {
735
- // Update existing action
736
- const newActions = [...x];
737
- newActions[existingActionIndex]!.state = state;
738
- newActions[existingActionIndex]!.message = msg;
739
- return newActions;
740
- } else {
741
- // Insert new action
742
- const fieldDescriptor: FieldDescriptor = {
743
- fieldId: fieldId,
744
- item: item,
745
- };
746
-
747
- const newAction: FieldAction = {
748
- field: fieldDescriptor,
749
- state,
750
- message: msg,
751
- label: label,
752
- };
753
- // console.log(newAction);
754
- return [...x, newAction];
755
- }
756
- });
757
- }
758
-
759
- if (message.type === "update-quota") {
760
- setQuotaInfo(message.payload);
761
- }
762
-
763
- if (message.type === "agent-started") {
764
- // Agent started message will be handled by individual components that subscribe
765
- // The payload should contain { agentId: string, agentName: string }
766
- }
767
-
768
- if (message.type === "load-item") {
769
- const itemDescriptor = message.payload as ItemDescriptor;
770
- if (itemDescriptor) {
771
- loadItem(itemDescriptor, { skipViewChange: true });
772
- }
773
- }
774
-
775
- if (message.type === "edit-operation") {
776
- const op = message.payload as EditOperation;
777
-
778
- if (op.type === "edit-field") {
779
- const editFieldOperation = op as EditFieldOperation;
780
-
781
- const field = await itemsRepository.getField({
782
- item: {
783
- ...editFieldOperation.mainItem,
784
- id: editFieldOperation.itemId,
785
- },
786
- fieldId: editFieldOperation.fieldId,
787
- });
788
-
789
- if (
790
- !field ||
791
- (field.type !== "single-line text" &&
792
- field.type !== "multi-line text" &&
793
- field.type !== "rich text")
794
- ) {
795
- itemsRepository.refreshItems([
796
- {
797
- ...editFieldOperation.mainItem,
798
- id: editFieldOperation.itemId,
799
- },
800
- ]);
801
- requestRefresh("immediate");
802
- }
803
- //TODO: field value changes that require rerender
804
- else
805
- itemsRepository.updateFieldValue(
806
- {
807
- fieldId: editFieldOperation.fieldId,
808
- item: {
809
- ...editFieldOperation.mainItem,
810
- id: editFieldOperation.itemId,
811
- },
812
- },
813
- editFieldOperation.user ?? { name: "unknown", ai: false },
814
- false,
815
- editFieldOperation.undone
816
- ? editFieldOperation.oldValue
817
- : editFieldOperation.value,
818
- );
819
- } else {
820
- requestRefresh("immediate");
821
- }
822
-
823
- if (
824
- op.mainItem &&
825
- op.mainItem.id === currentItemRef.current?.descriptor.id &&
826
- op.mainItem.language ===
827
- currentItemRef.current?.descriptor.language &&
828
- op.mainItem.version === currentItemRef.current?.descriptor.version
829
- ) {
830
- loadHistory(op.mainItem);
831
- }
832
- }
833
-
834
- socketMessageListeners.current.forEach((listener) => listener(message));
835
- },
836
- [currentItemDescriptorRef, addRecentEdit],
837
- );
549
+ // messageHandler is defined after loadItem/loadHistory declarations to avoid temporal dead zones
838
550
 
839
551
  const user = useMemo(
840
552
  () =>
@@ -877,10 +589,10 @@ export function EditorClient({
877
589
  } else {
878
590
  // Force a reconnect to refresh presence after several failed nudges
879
591
  try {
880
- socketInstanceRef.current?.close(4000, "recover-presence");
881
- } catch {
882
- // Ignore errors while attempting to recover presence
883
- }
592
+ (
593
+ (globalThis as any).editorSocket as WebSocket | null | undefined
594
+ )?.close(4000, "recover-presence");
595
+ } catch {}
884
596
  }
885
597
  }, delay);
886
598
 
@@ -1003,31 +715,8 @@ export function EditorClient({
1003
715
  };
1004
716
  }, []);
1005
717
 
1006
- // Custom hook for responsive design
1007
- const useMediaQuery = (query: string) => {
1008
- const [matches, setMatches] = useState(false);
1009
-
1010
- useEffect(() => {
1011
- const media = window.matchMedia(query);
1012
- const updateMatch = () => setMatches(media.matches);
1013
-
1014
- // Set initial value
1015
- updateMatch();
1016
-
1017
- // Listen for changes
1018
- media.addEventListener("change", updateMatch);
1019
-
1020
- // Cleanup
1021
- return () => {
1022
- media.removeEventListener("change", updateMatch);
1023
- };
1024
- }, [query]);
1025
-
1026
- return matches;
1027
- };
1028
-
1029
- // Detect mobile screens (max-width: 768px)
1030
718
  const isMobile = useMediaQuery("(max-width: 768px)");
719
+ const media = useMediaSelector();
1031
720
 
1032
721
  useEffect(() => {
1033
722
  // Suppress auto-start tour when `noTour` query parameter is present
@@ -1045,147 +734,9 @@ export function EditorClient({
1045
734
  }
1046
735
  }, [user]);
1047
736
 
1048
- useEffect(() => {
1049
- let reconnectTimeout: NodeJS.Timeout | null = null;
1050
- let reconnectAttempts = 0;
1051
-
1052
- const connectWebSocket = () => {
1053
- let socket: WebSocket | undefined = (globalThis as any).editorSocket;
1054
-
1055
- const needsNewSocket =
1056
- !socket ||
1057
- socket.readyState === WebSocket.CLOSING ||
1058
- socket.readyState === WebSocket.CLOSED;
1059
-
1060
- if (needsNewSocket) {
1061
- socket = connectSocket(sessionId);
1062
-
1063
- // Connection opened
1064
- socket.addEventListener("open", () => {
1065
- console.log("Connected!");
1066
- reconnectAttempts = 0; // Reset attempts on successful connection
1067
- sendClientInfo();
1068
- requestQuota();
1069
- //TODO: Load clients
1070
- });
1071
-
1072
- // Handle connection close
1073
- socket.addEventListener("close", (event) => {
1074
- // WebSocket connection closed
1075
-
1076
- // Only attempt to reconnect if it wasn't a clean close
1077
- if (event.code !== 1000) {
1078
- // Start with 1 second, increase exponentially up to 30 seconds
1079
- const delay = Math.min(
1080
- 1000 * Math.pow(2, Math.min(reconnectAttempts, 5)),
1081
- 30000,
1082
- );
1083
- // Attempting to reconnect with backoff
1084
-
1085
- reconnectTimeout = setTimeout(() => {
1086
- reconnectAttempts++;
1087
- connectWebSocket();
1088
- }, delay);
1089
- }
1090
- });
737
+ // WebSocket initialization is performed after messageHandler is defined
1091
738
 
1092
- // Handle connection errors
1093
- socket.addEventListener("error", (error) => {
1094
- console.error("WebSocket error:", error);
1095
- });
1096
-
1097
- (globalThis as any).editorSocket = socket;
1098
- }
1099
-
1100
- // Always ensure this instance is listening to messages
1101
- if (socket) {
1102
- socket.addEventListener("message", messageHandler);
1103
- socketInstanceRef.current = socket;
1104
-
1105
- // If we attached to an already-open socket, resend client info to refresh presence
1106
- if (socket.readyState === WebSocket.OPEN) {
1107
- sendClientInfo();
1108
- requestQuota();
1109
- }
1110
- }
1111
- };
1112
-
1113
- connectWebSocket();
1114
-
1115
- // Cleanup function
1116
- return () => {
1117
- if (reconnectTimeout) {
1118
- clearTimeout(reconnectTimeout);
1119
- }
1120
- if (socketInstanceRef.current) {
1121
- socketInstanceRef.current.removeEventListener(
1122
- "message",
1123
- messageHandler,
1124
- );
1125
- socketInstanceRef.current = null;
1126
- }
1127
- };
1128
- }, []);
1129
-
1130
- // Handle initial state setup from URL (only on initial load)
1131
- useEffect(() => {
1132
- if (!isInitialLoad) return;
1133
-
1134
- const itemid = searchParams.get("itemid");
1135
- const wizardId = searchParams.get("wizardid");
1136
- const urlView = searchParams.get("view");
1137
-
1138
- // Handle wizard ID from URL first (before view)
1139
- if (wizardId) {
1140
- setCurrentWizardId(wizardId);
1141
- }
1142
-
1143
- if (urlView) {
1144
- setViewName(urlView);
1145
- } else if (!itemid) {
1146
- setViewName("splash-screen");
1147
- }
1148
- }, [searchParams, isInitialLoad]);
1149
-
1150
- // Handle initial compare mode from URL (only on initial load)
1151
- useEffect(() => {
1152
- if (!isInitialLoad) return;
1153
-
1154
- if (searchParams.has("compare")) {
1155
- const compareValue = searchParams.get("compare") === "true";
1156
- setCompareMode(compareValue);
1157
- }
1158
- }, [searchParams, pathname, isInitialLoad]);
1159
-
1160
- // Handle initial item loading from URL and loadItemDescriptor
1161
- useEffect(() => {
1162
- // Use URL params only on initial load, otherwise respect loadItemDescriptor prop
1163
- const itemid = isInitialLoad ? searchParams.get("itemid") : null;
1164
- const itemId = cleanId(loadItemDescriptor?.id ?? itemid ?? undefined);
1165
- const language =
1166
- loadItemDescriptor?.language ??
1167
- (isInitialLoad
1168
- ? (searchParams.get("lang") ?? searchParams.get("language"))
1169
- : null);
1170
- const version =
1171
- loadItemDescriptor?.version ??
1172
- (isInitialLoad && searchParams.has("version")
1173
- ? parseInt(searchParams.get("version")!)
1174
- : 0);
1175
-
1176
- if (!itemId || !language) return;
1177
-
1178
- // Skip loading if the item is already loaded with the same parameters
1179
- if (
1180
- currentItemDescriptor?.id === itemId &&
1181
- currentItemDescriptor?.language === language &&
1182
- (!version || currentItemDescriptor?.version === version)
1183
- ) {
1184
- return;
1185
- }
1186
-
1187
- loadItem({ id: itemId, language, version });
1188
- }, [loadItemDescriptor, searchParams, currentItemDescriptor, isInitialLoad]);
739
+ // Defer URL sync until loadItem is defined below
1189
740
 
1190
741
  // Mark end of initial load phase
1191
742
  useEffect(() => {
@@ -1312,32 +863,7 @@ export function EditorClient({
1312
863
  if (!isLoading) setInserting(undefined);
1313
864
  }, [page, viewName, revision]);
1314
865
 
1315
- useEffect(() => {
1316
- // Handle fullscreen on initial load only
1317
- if (
1318
- isInitialLoad &&
1319
- (searchParams.get("fullscreen") || configuration.forceFullscreen)
1320
- ) {
1321
- pageViewContext.setFullscreen(true);
1322
- }
1323
- const handleMessage = (event: MessageEvent) => {
1324
- if (event.data.action === "refresh") {
1325
- requestRefresh("immediate");
1326
- }
1327
- };
1328
-
1329
- window.addEventListener("message", handleMessage);
1330
-
1331
- return () => {
1332
- window.removeEventListener("message", handleMessage);
1333
- };
1334
- }, [
1335
- isInitialLoad,
1336
- searchParams,
1337
- pathname,
1338
- pageViewContext,
1339
- configuration.forceFullscreen,
1340
- ]);
866
+ // moved below to ensure requestRefresh is declared first
1341
867
 
1342
868
  const loadHistory = useDebouncedCallback(async (item: ItemDescriptor) => {
1343
869
  const result = await getEditHistory(item);
@@ -1351,6 +877,8 @@ export function EditorClient({
1351
877
  setEditHistory(result.data || []);
1352
878
  }, []);
1353
879
 
880
+ // defined below after loadItem/loadItemVersions/requestRefresh
881
+
1354
882
  const requestRefresh = useCallback(
1355
883
  (mode?: "immediate" | "delayed" | "waitForQuietPeriod") => {
1356
884
  const refreshTimer = (globalThis as any).editorRefreshTimer;
@@ -1593,6 +1121,22 @@ export function EditorClient({
1593
1121
  ],
1594
1122
  );
1595
1123
 
1124
+ useEditorUrlSync({
1125
+ isInitialLoad,
1126
+ setIsInitialLoad,
1127
+ searchParams,
1128
+ pathname,
1129
+ setCurrentWizardId,
1130
+ setViewName,
1131
+ setCompareMode,
1132
+ loadItem,
1133
+ currentItemDescriptor,
1134
+ loadItemDescriptor,
1135
+ routerPush: (url: string) => router.push(url, { scroll: false }),
1136
+ });
1137
+
1138
+ // initialized below after messageHandler
1139
+
1596
1140
  useEffect(() => {
1597
1141
  if (
1598
1142
  pageViewContext.fullscreen &&
@@ -1729,107 +1273,44 @@ export function EditorClient({
1729
1273
  [showErrorToast, confirmationDialogRef, onOperationExecuted],
1730
1274
  );
1731
1275
 
1732
- const selectMedia = useCallback(
1733
- ({
1734
- selectedIdPath,
1735
- mode,
1736
- }: {
1737
- selectedIdPath: string;
1738
- mode?: MediaSelectorMode;
1739
- }) => {
1740
- setSelectedMediaIdPath(selectedIdPath);
1741
- setMediaSelectorVisible(true);
1742
- if (mode) setMediaSelectorMode(mode);
1743
- return new Promise<string>((resolve) => {
1744
- setMediaResolver(() => resolve);
1745
- });
1746
- },
1747
- [],
1748
- );
1749
-
1750
- const onMediaSelect = useCallback(
1751
- (mediaUrl: string) => {
1752
- mediaResolver?.(mediaUrl);
1753
- setMediaSelectorVisible(false);
1754
- setMediaResolver(undefined);
1755
- },
1756
- [mediaResolver],
1757
- );
1758
-
1759
- useEffect(() => {
1760
- if (!workboxItems || workboxItems.length === 0) return;
1761
- const itemsToValidate = workboxItems.map((x) => x.item);
1762
- validate(itemsToValidate);
1763
- }, [workboxItems]);
1764
-
1765
- async function loadWorkbox(items: ItemDescriptor[]) {
1766
- if (!items.length) {
1767
- setWorkboxItems([]);
1768
- return;
1769
- }
1770
-
1771
- const workbox = await getWorkbox(items.map((x) => getItemDescriptor(x)));
1772
- const workboxItems: WorkboxItem[] = workbox.data || [];
1773
-
1774
- const sortedWorkboxItems = workboxItems.sort((a, b) => {
1775
- if (a.isPublished === b.isPublished)
1776
- return (
1777
- (b.workflowCommands?.length || 0) - (a.workflowCommands?.length || 0)
1778
- );
1779
- return !a.isPublished || !a.isPublishable ? -1 : 1;
1780
- });
1781
-
1782
- setWorkboxItems(sortedWorkboxItems);
1783
- }
1784
-
1785
- const loadWorkboxDebounced = useDebouncedCallback(
1786
- (items: ItemDescriptor[]) => loadWorkbox(items),
1787
- 5000,
1788
- );
1789
-
1790
- useEffect(() => {
1791
- const items: ItemDescriptor[] = [];
1792
-
1793
- if (editContext.contentEditorItem) {
1794
- items.push(editContext.contentEditorItem.descriptor);
1795
- }
1276
+ // moved to useMediaSelector
1796
1277
 
1797
- if (editContext.page) {
1798
- collectAllItems(editContext.page.rootComponent, items);
1799
- }
1800
-
1801
- loadWorkboxDebounced(items.filter((x) => x));
1802
- }, [page, contentEditorItem]);
1278
+ const { workboxItems } = useWorkbox({
1279
+ page,
1280
+ contentEditorItem,
1281
+ validate,
1282
+ });
1803
1283
 
1804
- function collectAllItems(component: Component, items: ItemDescriptor[]) {
1805
- component.placeholders.forEach((x) => {
1806
- x.components.forEach((y) => {
1807
- if (y.isShared && y.datasourceItem) {
1808
- items.push(y.datasourceItem);
1809
- }
1284
+ // WebSocket message handler and connection
1285
+ const messageHandler = useSocketMessageHandler({
1286
+ sessionId,
1287
+ setWebSocketMessages,
1288
+ setActiveSessions,
1289
+ sendClientInfo,
1290
+ itemsRepository,
1291
+ currentItemDescriptor,
1292
+ loadItem,
1293
+ loadItemVersions,
1294
+ setComments,
1295
+ setSuggestedEdits,
1296
+ setActiveFieldActions,
1297
+ setQuotaInfo,
1298
+ requestRefresh,
1299
+ currentItemRef,
1300
+ loadHistory,
1301
+ addRecentEdit,
1302
+ socketMessageListeners,
1303
+ });
1810
1304
 
1811
- //TODO: Add picture fields
1812
- // y.datasourceItem?.fields.forEach((z) => {
1813
- // if (z.type === "picture") {
1814
- // const picture = z.value as PictureValue;
1815
- // if (picture.variants) {
1816
- // picture.variants.forEach((v) => {
1817
- // if (v.mediaId) {
1818
- // items.push({
1819
- // id: v.mediaId,
1820
- // language: y.datasourceItem!.descriptor.language,
1821
- // version: 0,
1822
- // });
1823
- // }
1824
- // });
1825
- // }
1826
- // }
1827
- // });
1828
-
1829
- collectAllItems(y, items);
1830
- });
1831
- });
1832
- }
1305
+ const { socketRef: socketInstanceRef } = useEditorWebSocket({
1306
+ sessionId,
1307
+ onMessage: messageHandler,
1308
+ onOpen: () => console.log("Connected!"),
1309
+ onError: (error) => console.error("WebSocket error:", error),
1310
+ connectSocket,
1311
+ requestQuota,
1312
+ sendClientInfo,
1313
+ });
1833
1314
 
1834
1315
  const switchView = (
1835
1316
  viewName: string,
@@ -1945,18 +1426,10 @@ export function EditorClient({
1945
1426
  executeCommand,
1946
1427
  });
1947
1428
 
1948
- if (typeof window !== "undefined")
1949
- useEventListenerExt("keydown", handleKeyDown, window, true);
1950
-
1951
- if (typeof window !== "undefined")
1952
- useEventListenerExt(
1953
- "click",
1954
- () => {
1955
- contextMenuRef.current?.close({});
1956
- },
1957
- window,
1958
- true,
1959
- );
1429
+ useGlobalEditorEvents({
1430
+ onKeyDown: handleKeyDown,
1431
+ onWindowClick: () => contextMenuRef.current?.close({}),
1432
+ });
1960
1433
 
1961
1434
  useEffect(() => {
1962
1435
  if (mode === "suggestions") {
@@ -1987,71 +1460,7 @@ export function EditorClient({
1987
1460
  );
1988
1461
 
1989
1462
  // Quota checking functions
1990
- const isQuotaExceeded = useCallback(() => {
1991
- if (!quotaInfo) return false;
1992
-
1993
- const { usage, limits } = quotaInfo;
1994
-
1995
- // Check absolute limits
1996
- if (limits.totalTokens > 0 && usage.totalTokens >= limits.totalTokens)
1997
- return true;
1998
- if (limits.totalImages > 0 && usage.totalImages >= limits.totalImages)
1999
- return true;
2000
-
2001
- // For now, we're only checking absolute limits as daily/monthly would require server-side logic
2002
- // You can extend this to check daily/monthly limits if the server provides that information
2003
-
2004
- return false;
2005
- }, [quotaInfo]);
2006
-
2007
- const getQuotaWarningMessage = useCallback(() => {
2008
- if (!quotaInfo) return null;
2009
-
2010
- const { usage, limits } = quotaInfo;
2011
- const warnings: string[] = [];
2012
-
2013
- // Check tokens
2014
- if (limits.totalTokens > 0) {
2015
- const tokenPercentage = (usage.totalTokens / limits.totalTokens) * 100;
2016
- if (tokenPercentage >= 100) {
2017
- warnings.push(
2018
- `Token limit exceeded (${usage.totalTokens}/${limits.totalTokens})`,
2019
- );
2020
- } else if (tokenPercentage >= 90) {
2021
- warnings.push(
2022
- `Token usage high: ${Math.round(tokenPercentage)}% (${usage.totalTokens}/${limits.totalTokens})`,
2023
- );
2024
- }
2025
- }
2026
-
2027
- // Check images
2028
- if (limits.totalImages > 0) {
2029
- const imagePercentage = (usage.totalImages / limits.totalImages) * 100;
2030
- if (imagePercentage >= 100) {
2031
- warnings.push(
2032
- `Image limit exceeded (${usage.totalImages}/${limits.totalImages})`,
2033
- );
2034
- } else if (imagePercentage >= 90) {
2035
- warnings.push(
2036
- `Image usage high: ${Math.round(imagePercentage)}% (${usage.totalImages}/${limits.totalImages})`,
2037
- );
2038
- }
2039
- }
2040
-
2041
- return warnings.length > 0 ? warnings.join(", ") : null;
2042
- }, [quotaInfo]);
2043
-
2044
- // Show warning when quota is exceeded
2045
- useEffect(() => {
2046
- const warningMessage = getQuotaWarningMessage();
2047
- if (warningMessage) {
2048
- const isExceeded = isQuotaExceeded();
2049
- showErrorToast({
2050
- summary: isExceeded ? "AI Quota Exceeded" : "AI Quota Warning",
2051
- details: warningMessage,
2052
- });
2053
- }
2054
- }, [quotaInfo, getQuotaWarningMessage, isQuotaExceeded, showErrorToast]);
1463
+ // moved into hook
2055
1464
 
2056
1465
  // Calculate visible views separately to avoid circular dependency
2057
1466
  const visibleViews = useMemo(() => {
@@ -2126,7 +1535,7 @@ export function EditorClient({
2126
1535
 
2127
1536
  router.push(newUrl, { scroll: false });
2128
1537
  },
2129
- selectMedia,
1538
+ selectMedia: media.selectMedia,
2130
1539
  showToast: (message: string) => {
2131
1540
  toast(message);
2132
1541
  },
@@ -2412,7 +1821,7 @@ export function EditorClient({
2412
1821
  if (!descriptor) return;
2413
1822
 
2414
1823
  const itemId =
2415
- focusedField?.item.id ||
1824
+ focusedFieldRef.current?.item.id ||
2416
1825
  (selection.length > 0 ? selection[0] : undefined) ||
2417
1826
  descriptor.id;
2418
1827
 
@@ -2422,8 +1831,8 @@ export function EditorClient({
2422
1831
  const version = descriptor.version;
2423
1832
 
2424
1833
  const getFieldName = async () => {
2425
- if (!focusedField) return "";
2426
- const field = await itemsRepository.getField(focusedField);
1834
+ if (!focusedFieldRef.current) return "";
1835
+ const field = await itemsRepository.getField(focusedFieldRef.current);
2427
1836
  return field?.name;
2428
1837
  };
2429
1838
 
@@ -2441,7 +1850,7 @@ export function EditorClient({
2441
1850
  isNew: true,
2442
1851
  itemId,
2443
1852
  itemName: await getItemName(),
2444
- fieldId: focusedField?.fieldId,
1853
+ fieldId: focusedFieldRef.current?.fieldId,
2445
1854
  fieldName: await getFieldName(),
2446
1855
  mainItemId: descriptor.id,
2447
1856
  language,
@@ -2450,7 +1859,7 @@ export function EditorClient({
2450
1859
  rangeStart: selectedRange?.startOffset || 0,
2451
1860
  rangeEnd: selectedRange?.endOffset || 0,
2452
1861
  author: user?.name,
2453
- authorDisplayName: user?.displayName,
1862
+ authorDisplayName: user?.fullName,
2454
1863
  date: new Date().toISOString(),
2455
1864
  };
2456
1865
 
@@ -2517,7 +1926,6 @@ export function EditorClient({
2517
1926
  searchParams,
2518
1927
  pathname,
2519
1928
  router,
2520
- selectMedia,
2521
1929
  scrollIntoView,
2522
1930
  focusedField,
2523
1931
  renderedFields,
@@ -2864,56 +2272,13 @@ export function EditorClient({
2864
2272
  compareView={compareMode}
2865
2273
  pageViewContext={pageViewContext}
2866
2274
  />
2867
- {/* Control buttons in top right corner */}
2868
- <div className="fixed top-4 right-4 z-[9999] flex gap-2">
2869
- {/* Device toggle button */}
2870
- <button
2871
- onClick={() => {
2872
- const currentDevice = pageViewContext.device;
2873
- if (currentDevice === "desktop") {
2874
- // Switch to mobile (first mobile device from configuration)
2875
- const firstMobileDevice = configuration.devices[0];
2876
- if (firstMobileDevice) {
2877
- pageViewContext.setDevice(firstMobileDevice.name);
2878
- }
2879
- } else {
2880
- // Switch to desktop
2881
- pageViewContext.setDevice("desktop");
2882
- }
2883
- }}
2884
- 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"
2885
- aria-label={
2886
- pageViewContext.device === "desktop"
2887
- ? "Switch to mobile view"
2888
- : "Switch to desktop view"
2889
- }
2890
- title={
2891
- pageViewContext.device === "desktop"
2892
- ? "Switch to mobile view"
2893
- : "Switch to desktop view"
2894
- }
2895
- data-testid="fullscreen-device-toggle"
2896
- >
2897
- {pageViewContext.device === "desktop" ? (
2898
- <Smartphone className="h-5 w-5" />
2899
- ) : (
2900
- <Monitor className="h-5 w-5" />
2901
- )}
2902
- </button>
2903
-
2904
- {/* Exit fullscreen button */}
2905
- {!configuration.forceFullscreen && (
2906
- <button
2907
- onClick={() => pageViewContext.setFullscreen(false)}
2908
- 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"
2909
- aria-label="Exit fullscreen"
2910
- title="Return to normal view"
2911
- data-testid="fullscreen-exit-button"
2912
- >
2913
- <Shrink className="h-5 w-5" />
2914
- </button>
2915
- )}
2916
- </div>
2275
+ <FullscreenControls
2276
+ device={pageViewContext.device}
2277
+ setDevice={(d) => pageViewContext.setDevice(d)}
2278
+ canExit={!configuration.forceFullscreen}
2279
+ onExit={() => pageViewContext.setFullscreen(false)}
2280
+ firstMobileDeviceName={configuration.devices[0]?.name}
2281
+ />
2917
2282
  </div>
2918
2283
  <EditorFormPopup
2919
2284
  ref={editorFormPopupRef}
@@ -2937,49 +2302,16 @@ export function EditorClient({
2937
2302
  </>
2938
2303
  ) : (
2939
2304
  <>
2940
- <MainLayout
2305
+ <EditorChrome
2941
2306
  className={className}
2942
- view={currentView}
2307
+ currentView={currentView}
2943
2308
  centerPanelView={centerPanelView}
2944
- rightSidebar={
2945
- currentView.rightSidebar &&
2946
- editContext.page &&
2947
- showComponentNavigator && (
2948
- <SidebarView
2949
- sidebar={currentView.rightSidebar}
2950
- editContext={editContext}
2951
- active={true}
2952
- detached={true}
2953
- onClose={() => handleSetShowComponentNavigator(false)}
2954
- />
2955
- )
2956
- }
2957
- rightSidebarTitle={currentView.rightSidebar?.title}
2958
- farRightSidebar={
2959
- showAgentsPanel &&
2960
- !["splash-screen", "open-page", "new-page"].includes(viewName) && (
2961
- <SidebarView
2962
- sidebar={{
2963
- title: "Agents",
2964
- panels: [
2965
- {
2966
- name: "agents",
2967
- title: "Agents",
2968
- content: <Agents />,
2969
- initialSize: 70,
2970
- noOverflow: true,
2971
- },
2972
- ],
2973
- }}
2974
- editContext={editContext}
2975
- active={true}
2976
- detached={true}
2977
- paddingRight={true}
2978
- onClose={() => handleSetShowAgentsPanel(false)}
2979
- />
2980
- )
2981
- }
2982
- farRightSidebarTitle={"AGENTS"}
2309
+ editContext={editContext}
2310
+ showComponentNavigator={showComponentNavigator}
2311
+ handleSetShowComponentNavigator={handleSetShowComponentNavigator}
2312
+ showAgentsPanel={showAgentsPanel}
2313
+ handleSetShowAgentsPanel={handleSetShowAgentsPanel}
2314
+ viewName={viewName}
2983
2315
  />
2984
2316
 
2985
2317
  {isTourActive && <Tour tourStopCallback={() => setIsTourActive(false)} />}
@@ -3001,14 +2333,14 @@ export function EditorClient({
3001
2333
  <Toaster position="top-center" />{" "}
3002
2334
  <ConfirmationDialog ref={confirmationDialogRef} />
3003
2335
  <EditContextMenu ref={contextMenuRef} />
3004
- {mediaSelectorVisible && (
2336
+ {media.mediaSelectorVisible && (
3005
2337
  <MediaSelector
3006
2338
  language={editContext.currentItemDescriptor!.language}
3007
- visible={mediaSelectorVisible}
3008
- onHide={() => setMediaSelectorVisible(false)}
3009
- onMediaSelected={onMediaSelect}
3010
- selectedIdPath={selectedMediaIdPath}
3011
- mode={mediaSelectorMode}
2339
+ visible={media.mediaSelectorVisible}
2340
+ onHide={() => media.setMediaSelectorVisible(false)}
2341
+ onMediaSelected={media.onMediaSelect}
2342
+ selectedIdPath={media.selectedMediaIdPath}
2343
+ mode={media.mediaSelectorMode}
3012
2344
  />
3013
2345
  )}
3014
2346
  <FieldEditorPopup ref={fieldEditorPopupRef} />