@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.
- package/dist/components/ui/context-menu.js +1 -1
- package/dist/components/ui/context-menu.js.map +1 -1
- package/dist/config/types.d.ts +1 -0
- package/dist/editor/ContextMenu.js +25 -14
- package/dist/editor/ContextMenu.js.map +1 -1
- package/dist/editor/Editor.js +3 -3
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/FieldListField.js +1 -1
- package/dist/editor/FieldListField.js.map +1 -1
- package/dist/editor/ai/Agents.js +7 -0
- package/dist/editor/ai/Agents.js.map +1 -1
- package/dist/editor/client/{EditorClient.d.ts → EditorShell.d.ts} +1 -19
- package/dist/editor/client/{EditorClient.js → EditorShell.js} +100 -575
- package/dist/editor/client/EditorShell.js.map +1 -0
- package/dist/editor/client/editContext.d.ts +2 -1
- package/dist/editor/client/hooks/useEditorUrlSync.d.ts +18 -0
- package/dist/editor/client/hooks/useEditorUrlSync.js +56 -0
- package/dist/editor/client/hooks/useEditorUrlSync.js.map +1 -0
- package/dist/editor/client/hooks/useEditorWebSocket.d.ts +11 -0
- package/dist/editor/client/hooks/useEditorWebSocket.js +86 -0
- package/dist/editor/client/hooks/useEditorWebSocket.js.map +1 -0
- package/dist/editor/client/hooks/useGlobalEditorEvents.d.ts +4 -0
- package/dist/editor/client/hooks/useGlobalEditorEvents.js +15 -0
- package/dist/editor/client/hooks/useGlobalEditorEvents.js.map +1 -0
- package/dist/editor/client/hooks/useMediaQuery.d.ts +1 -0
- package/dist/editor/client/hooks/useMediaQuery.js +19 -0
- package/dist/editor/client/hooks/useMediaQuery.js.map +1 -0
- package/dist/editor/client/hooks/useMediaSelector.d.ts +12 -0
- package/dist/editor/client/hooks/useMediaSelector.js +30 -0
- package/dist/editor/client/hooks/useMediaSelector.js.map +1 -0
- package/dist/editor/client/hooks/useQuota.d.ts +29 -0
- package/dist/editor/client/hooks/useQuota.js +53 -0
- package/dist/editor/client/hooks/useQuota.js.map +1 -0
- package/dist/editor/client/hooks/useSocketMessageHandler.d.ts +34 -0
- package/dist/editor/client/hooks/useSocketMessageHandler.js +191 -0
- package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -0
- package/dist/editor/client/hooks/useWorkbox.d.ts +9 -0
- package/dist/editor/client/hooks/useWorkbox.js +53 -0
- package/dist/editor/client/hooks/useWorkbox.js.map +1 -0
- package/dist/editor/client/ui/EditorChrome.d.ts +12 -0
- package/dist/editor/client/ui/EditorChrome.js +23 -0
- package/dist/editor/client/ui/EditorChrome.js.map +1 -0
- package/dist/editor/client/ui/FullscreenControls.d.ts +7 -0
- package/dist/editor/client/ui/FullscreenControls.js +21 -0
- package/dist/editor/client/ui/FullscreenControls.js.map +1 -0
- package/dist/editor/commands/componentCommands.js +6 -1
- package/dist/editor/commands/componentCommands.js.map +1 -1
- package/dist/editor/control-center/Info.js +1 -1
- package/dist/editor/control-center/Info.js.map +1 -1
- package/dist/editor/control-center/WebSocketMessages.js.map +1 -1
- package/dist/editor/menubar/ActiveUsers.js +3 -3
- package/dist/editor/menubar/ActiveUsers.js.map +1 -1
- package/dist/editor/menubar/User.js +4 -2
- package/dist/editor/menubar/User.js.map +1 -1
- package/dist/editor/page-editor-chrome/FieldActionIndicator.d.ts +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +5 -7
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZones.js +11 -3
- package/dist/editor/page-editor-chrome/PlaceholderDropZones.js.map +1 -1
- package/dist/editor/reviews/CommentPopover.d.ts +9 -2
- package/dist/editor/reviews/CommentPopover.js +12 -10
- package/dist/editor/reviews/CommentPopover.js.map +1 -1
- package/dist/editor/reviews/PreviewInfo.js +1 -1
- package/dist/editor/reviews/PreviewInfo.js.map +1 -1
- package/dist/editor/reviews/Reviews.js +2 -2
- package/dist/editor/reviews/Reviews.js.map +1 -1
- package/dist/editor/ui/Icons.js +1 -1
- package/dist/editor/ui/Icons.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/ui/context-menu.tsx +1 -1
- package/src/config/types.ts +1 -0
- package/src/editor/ContextMenu.tsx +54 -34
- package/src/editor/Editor.tsx +14 -5
- package/src/editor/FieldListField.tsx +1 -1
- package/src/editor/ai/Agents.tsx +6 -0
- package/src/editor/client/{EditorClient.tsx → EditorShell.tsx} +124 -792
- package/src/editor/client/editContext.ts +3 -3
- package/src/editor/client/hooks/useEditorUrlSync.ts +83 -0
- package/src/editor/client/hooks/useEditorWebSocket.ts +122 -0
- package/src/editor/client/hooks/useGlobalEditorEvents.ts +18 -0
- package/src/editor/client/hooks/useMediaQuery.ts +21 -0
- package/src/editor/client/hooks/useMediaSelector.ts +48 -0
- package/src/editor/client/hooks/useQuota.ts +81 -0
- package/src/editor/client/hooks/useSocketMessageHandler.ts +285 -0
- package/src/editor/client/hooks/useWorkbox.ts +67 -0
- package/src/editor/client/ui/EditorChrome.tsx +76 -0
- package/src/editor/client/ui/FullscreenControls.tsx +58 -0
- package/src/editor/commands/componentCommands.tsx +6 -1
- package/src/editor/control-center/Info.tsx +9 -7
- package/src/editor/control-center/WebSocketMessages.tsx +1 -2
- package/src/editor/menubar/ActiveUsers.tsx +7 -5
- package/src/editor/menubar/User.tsx +5 -2
- package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +1 -1
- package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +9 -8
- package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +14 -3
- package/src/editor/reviews/CommentPopover.tsx +20 -9
- package/src/editor/reviews/PreviewInfo.tsx +6 -6
- package/src/editor/reviews/Reviews.tsx +8 -2
- package/src/editor/ui/Icons.tsx +4 -4
- package/src/revision.ts +2 -2
- package/src/types.ts +1 -1
- 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 {
|
|
132
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
}, [page, contentEditorItem]);
|
|
1278
|
+
const { workboxItems } = useWorkbox({
|
|
1279
|
+
page,
|
|
1280
|
+
contentEditorItem,
|
|
1281
|
+
validate,
|
|
1282
|
+
});
|
|
1803
1283
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
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
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
2426
|
-
const field = await itemsRepository.getField(
|
|
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:
|
|
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?.
|
|
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
|
-
|
|
2868
|
-
|
|
2869
|
-
{
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
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
|
-
<
|
|
2305
|
+
<EditorChrome
|
|
2941
2306
|
className={className}
|
|
2942
|
-
|
|
2307
|
+
currentView={currentView}
|
|
2943
2308
|
centerPanelView={centerPanelView}
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
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} />
|