@parhelia/core 0.2.12783 → 0.2.12786
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/editor/ai/AgentImageCopyButton.d.ts +6 -0
- package/dist/editor/ai/AgentImageCopyButton.js +93 -0
- package/dist/editor/ai/AgentImageCopyButton.js.map +1 -0
- package/dist/editor/ai/terminal/components/EditFieldToolCallContent.d.ts +24 -0
- package/dist/editor/ai/terminal/components/EditFieldToolCallContent.js +97 -0
- package/dist/editor/ai/terminal/components/EditFieldToolCallContent.js.map +1 -0
- package/dist/editor/ai/terminal/components/ValueDiff.d.ts +15 -0
- package/dist/editor/ai/terminal/components/ValueDiff.js +21 -0
- package/dist/editor/ai/terminal/components/ValueDiff.js.map +1 -0
- package/dist/editor/ai/terminal/components/agentThinkingStyles.d.ts +1 -0
- package/dist/editor/ai/terminal/components/agentThinkingStyles.js +2 -0
- package/dist/editor/ai/terminal/components/agentThinkingStyles.js.map +1 -0
- package/dist/editor/ai/terminal/components/toolCallContentRenderers.d.ts +15 -0
- package/dist/editor/ai/terminal/components/toolCallContentRenderers.js +12 -0
- package/dist/editor/ai/terminal/components/toolCallContentRenderers.js.map +1 -0
- package/dist/editor/client/hooks/activeSessionSnapshots.d.ts +2 -0
- package/dist/editor/client/hooks/activeSessionSnapshots.js +43 -0
- package/dist/editor/client/hooks/activeSessionSnapshots.js.map +1 -0
- package/dist/editor/client/hooks/operationRefreshDeduplication.d.ts +22 -0
- package/dist/editor/client/hooks/operationRefreshDeduplication.js +45 -0
- package/dist/editor/client/hooks/operationRefreshDeduplication.js.map +1 -0
- package/dist/editor/client/hooks/socketMessageRetention.d.ts +7 -0
- package/dist/editor/client/hooks/socketMessageRetention.js +11 -0
- package/dist/editor/client/hooks/socketMessageRetention.js.map +1 -0
- package/dist/editor/client/hooks/useClientInfoRegistration.d.ts +13 -0
- package/dist/editor/client/hooks/useClientInfoRegistration.js +38 -0
- package/dist/editor/client/hooks/useClientInfoRegistration.js.map +1 -0
- package/dist/editor/client/undoTargetResolution.d.ts +28 -0
- package/dist/editor/client/undoTargetResolution.js +69 -0
- package/dist/editor/client/undoTargetResolution.js.map +1 -0
- package/dist/editor/field-types/singleLineTextValue.d.ts +3 -0
- package/dist/editor/field-types/singleLineTextValue.js +9 -0
- package/dist/editor/field-types/singleLineTextValue.js.map +1 -0
- package/dist/editor/insertOptionCompatibility.d.ts +7 -0
- package/dist/editor/insertOptionCompatibility.js +15 -0
- package/dist/editor/insertOptionCompatibility.js.map +1 -0
- package/dist/editor/media-selector/mediaItemDrag.d.ts +28 -0
- package/dist/editor/media-selector/mediaItemDrag.js +42 -0
- package/dist/editor/media-selector/mediaItemDrag.js.map +1 -0
- package/dist/editor/menubar/toolbar-sections/PreviewModeControl.d.ts +1 -0
- package/dist/editor/menubar/toolbar-sections/PreviewModeControl.js +14 -0
- package/dist/editor/menubar/toolbar-sections/PreviewModeControl.js.map +1 -0
- package/dist/editor/menubar/toolbar-sections/RefreshPageControl.d.ts +7 -0
- package/dist/editor/menubar/toolbar-sections/RefreshPageControl.js +44 -0
- package/dist/editor/menubar/toolbar-sections/RefreshPageControl.js.map +1 -0
- package/dist/editor/page-editor-chrome/InlineFieldHistoryButton.d.ts +58 -0
- package/dist/editor/page-editor-chrome/InlineFieldHistoryButton.js +731 -0
- package/dist/editor/page-editor-chrome/InlineFieldHistoryButton.js.map +1 -0
- package/dist/editor/page-viewer/bridgeInlineAiSelection.d.ts +17 -0
- package/dist/editor/page-viewer/bridgeInlineAiSelection.js +80 -0
- package/dist/editor/page-viewer/bridgeInlineAiSelection.js.map +1 -0
- package/dist/editor/page-viewer/bridgeInlineEditCommit.d.ts +18 -0
- package/dist/editor/page-viewer/bridgeInlineEditCommit.js +24 -0
- package/dist/editor/page-viewer/bridgeInlineEditCommit.js.map +1 -0
- package/dist/editor/page-viewer/bridgeItemChangeTarget.d.ts +9 -0
- package/dist/editor/page-viewer/bridgeItemChangeTarget.js +38 -0
- package/dist/editor/page-viewer/bridgeItemChangeTarget.js.map +1 -0
- package/dist/editor/page-viewer/bridgePointerDragRelease.d.ts +31 -0
- package/dist/editor/page-viewer/bridgePointerDragRelease.js +32 -0
- package/dist/editor/page-viewer/bridgePointerDragRelease.js.map +1 -0
- package/dist/editor/page-viewer/fullscreenPagePath.d.ts +1 -0
- package/dist/editor/page-viewer/fullscreenPagePath.js +11 -0
- package/dist/editor/page-viewer/fullscreenPagePath.js.map +1 -0
- package/dist/editor/page-viewer/internalLinkNavigation.d.ts +9 -0
- package/dist/editor/page-viewer/internalLinkNavigation.js +104 -0
- package/dist/editor/page-viewer/internalLinkNavigation.js.map +1 -0
- package/dist/editor/reviews/FeedbackFilters.d.ts +16 -0
- package/dist/editor/reviews/FeedbackFilters.js +45 -0
- package/dist/editor/reviews/FeedbackFilters.js.map +1 -0
- package/dist/editor/reviews/commentState.d.ts +4 -0
- package/dist/editor/reviews/commentState.js +15 -0
- package/dist/editor/reviews/commentState.js.map +1 -0
- package/dist/editor/settings/index/IndexGenerationsSection.d.ts +8 -0
- package/dist/editor/settings/index/IndexGenerationsSection.js +93 -0
- package/dist/editor/settings/index/IndexGenerationsSection.js.map +1 -0
- package/dist/editor/settings/panels/MemoriesPanel.d.ts +16 -0
- package/dist/editor/settings/panels/MemoriesPanel.js +292 -0
- package/dist/editor/settings/panels/MemoriesPanel.js.map +1 -0
- package/dist/editor/settings/panels/MemoryEntryDialog.d.ts +45 -0
- package/dist/editor/settings/panels/MemoryEntryDialog.js +167 -0
- package/dist/editor/settings/panels/MemoryEntryDialog.js.map +1 -0
- package/dist/editor/settings/panels/RenderingHostsPanel.d.ts +2 -0
- package/dist/editor/settings/panels/RenderingHostsPanel.js +63 -0
- package/dist/editor/settings/panels/RenderingHostsPanel.js.map +1 -0
- package/dist/editor/settings/panels/projectTemplateDuplication.d.ts +5 -0
- package/dist/editor/settings/panels/projectTemplateDuplication.js +57 -0
- package/dist/editor/settings/panels/projectTemplateDuplication.js.map +1 -0
- package/dist/editor/sidebar/WorkboxFilters.d.ts +11 -0
- package/dist/editor/sidebar/WorkboxFilters.js +61 -0
- package/dist/editor/sidebar/WorkboxFilters.js.map +1 -0
- package/dist/editor/version-diff/versionDiffGeometryKeys.d.ts +10 -0
- package/dist/editor/version-diff/versionDiffGeometryKeys.js +16 -0
- package/dist/editor/version-diff/versionDiffGeometryKeys.js.map +1 -0
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/package.json +1 -1
- package/dist/editor/ai/terminal/components/AgentEditHistoryButton.d.ts +0 -5
- package/dist/editor/ai/terminal/components/AgentEditHistoryButton.js +0 -12
- package/dist/editor/ai/terminal/components/AgentEditHistoryButton.js.map +0 -1
- package/dist/editor/ai/terminal/components/AgentThinkingDots.d.ts +0 -5
- package/dist/editor/ai/terminal/components/AgentThinkingDots.js +0 -9
- package/dist/editor/ai/terminal/components/AgentThinkingDots.js.map +0 -1
- package/dist/editor/reviews/CommentsAndFeedback.d.ts +0 -8
- package/dist/editor/reviews/CommentsAndFeedback.js +0 -17
- package/dist/editor/reviews/CommentsAndFeedback.js.map +0 -1
- package/dist/editor/reviews/CreateReviewDetailsStep.d.ts +0 -53
- package/dist/editor/reviews/CreateReviewDetailsStep.js +0 -77
- package/dist/editor/reviews/CreateReviewDetailsStep.js.map +0 -1
- package/dist/splash-screen/ParheliaAssistantChat.d.ts +0 -8
- package/dist/splash-screen/ParheliaAssistantChat.js +0 -507
- package/dist/splash-screen/ParheliaAssistantChat.js.map +0 -1
- package/dist/splash-screen/SplashScreenAgentContext.d.ts +0 -28
- package/dist/splash-screen/SplashScreenAgentContext.js +0 -51
- package/dist/splash-screen/SplashScreenAgentContext.js.map +0 -1
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, } from "react";
|
|
3
|
+
import { History, Loader2, Redo2, Undo2 } from "lucide-react";
|
|
4
|
+
import { useEditContext, useFieldsEditContext } from "../client/editContext";
|
|
5
|
+
import { INLINE_FIELD_HISTORY_STEP_EVENT } from "../page-viewer/bridgeKeyboardShortcuts";
|
|
6
|
+
import { formatDateOnly, normalizeMarkerId } from "../utils";
|
|
7
|
+
import { collapseFieldHistoryEntries, FieldHistory, shortenUserName, } from "../FieldHistory";
|
|
8
|
+
import { getFieldHistory } from "../services/editService";
|
|
9
|
+
import { Popover, PopoverContent, PopoverTrigger, } from "../../components/ui/popover";
|
|
10
|
+
import { Tooltip, TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip";
|
|
11
|
+
import { measureBridgeFieldRect, rectEquals, } from "./overlay/geometry";
|
|
12
|
+
import { useOverlayMeasurement } from "./overlay/useOverlayMeasurement";
|
|
13
|
+
export const INLINE_FIELD_HISTORY_BUTTON_GAP = 6;
|
|
14
|
+
export const INLINE_FIELD_HISTORY_BUTTON_SIZE = 24;
|
|
15
|
+
const QUICK_HISTORY_SESSION_TTL_MS = 15_000;
|
|
16
|
+
// Stepping patches the canvas immediately and coalesces the save, mirroring the
|
|
17
|
+
// trailing debounce typing already uses. Without it every step blocks the arrows
|
|
18
|
+
// on a server round-trip.
|
|
19
|
+
const QUICK_HISTORY_APPLY_DEBOUNCE_MS = 350;
|
|
20
|
+
// Long enough for the trailing typing save (its own debounce plus the server
|
|
21
|
+
// round-trip) to have appended the new history entry before we refetch.
|
|
22
|
+
const QUICK_HISTORY_REFRESH_DELAY_MS = 2_500;
|
|
23
|
+
/** Hovering the pill refetches at most this often; fresher data is kept. */
|
|
24
|
+
const QUICK_HISTORY_HOVER_STALE_MS = 10_000;
|
|
25
|
+
const QUICK_HISTORY_LABEL_MAX_WIDTH = 170;
|
|
26
|
+
const INLINE_FIELD_HISTORY_LABEL_GAP = 6;
|
|
27
|
+
/** The pill stays out of the way until the caret has been still this long. */
|
|
28
|
+
export const INLINE_FIELD_HISTORY_IDLE_DELAY_MS = 1_000;
|
|
29
|
+
/**
|
|
30
|
+
* Applying a step patches the canvas, which echoes back as a value change. That
|
|
31
|
+
* is our own doing, not the user typing, so it must not restart the idle wait
|
|
32
|
+
* and hide the pill mid-step. A refresh-driven re-render can trail the patch, so
|
|
33
|
+
* the window covers more than the echo itself.
|
|
34
|
+
*/
|
|
35
|
+
const INLINE_FIELD_HISTORY_SELF_CHANGE_WINDOW_MS = 2_500;
|
|
36
|
+
const quickHistorySessions = new Map();
|
|
37
|
+
/**
|
|
38
|
+
* The step position survives the pill unmounting (clicking away and back) for
|
|
39
|
+
* {@link QUICK_HISTORY_SESSION_TTL_MS}. Tests reset it so one case cannot leave
|
|
40
|
+
* the next one mid-history.
|
|
41
|
+
*/
|
|
42
|
+
export function clearInlineFieldHistoryQuickSessions() {
|
|
43
|
+
quickHistorySessions.clear();
|
|
44
|
+
}
|
|
45
|
+
function getQuickHistorySession(key) {
|
|
46
|
+
const session = quickHistorySessions.get(key);
|
|
47
|
+
if (!session)
|
|
48
|
+
return undefined;
|
|
49
|
+
if (session.expiresAt <= Date.now()) {
|
|
50
|
+
quickHistorySessions.delete(key);
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return session;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Places the pill for a text field above the start of the field's block,
|
|
57
|
+
* left-aligned with it. The block's top-left corner does not move as values
|
|
58
|
+
* change, so stepping through history never relocates the buttons under the
|
|
59
|
+
* pointer (a caret- or line-relative anchor did exactly that). When the block
|
|
60
|
+
* is at the very top of the visible canvas the pill flips under the block —
|
|
61
|
+
* still outside the text. Media fields anchor to the same top-left corner but
|
|
62
|
+
* sit just inside their bounds: the picture chrome owns the space above the
|
|
63
|
+
* image, and the corner keeps the trigger in the same fixed spot as on text
|
|
64
|
+
* fields.
|
|
65
|
+
*
|
|
66
|
+
* The overlay is clipped by the panel hosting the canvas, so a pill that would
|
|
67
|
+
* reach past the visible width drops its label rather than disappearing.
|
|
68
|
+
*/
|
|
69
|
+
export function calculateInlineFieldHistoryButtonPosition(anchorRect, options) {
|
|
70
|
+
const { anchor = "block", controlsWidth = 0, labelWidth = 0, viewportLeft = 0, viewportTop = 0, viewportWidth = 0, viewportHeight = 0, margin = 4, } = options ?? {};
|
|
71
|
+
const gap = INLINE_FIELD_HISTORY_BUTTON_GAP;
|
|
72
|
+
const labelledWidth = controlsWidth +
|
|
73
|
+
(labelWidth > 0 ? labelWidth + INLINE_FIELD_HISTORY_LABEL_GAP : 0);
|
|
74
|
+
const rightEdge = viewportLeft + viewportWidth - margin;
|
|
75
|
+
let left = anchor === "corner" ? anchorRect.x + gap : anchorRect.x;
|
|
76
|
+
let showLabel = true;
|
|
77
|
+
if (viewportWidth > 0 && controlsWidth > 0) {
|
|
78
|
+
showLabel = left + labelledWidth <= rightEdge;
|
|
79
|
+
const usedWidth = showLabel ? labelledWidth : controlsWidth;
|
|
80
|
+
left = Math.max(viewportLeft + margin, Math.min(left, rightEdge - usedWidth));
|
|
81
|
+
}
|
|
82
|
+
if (anchor === "corner") {
|
|
83
|
+
return {
|
|
84
|
+
left,
|
|
85
|
+
top: anchorRect.y + gap,
|
|
86
|
+
showLabel,
|
|
87
|
+
placement: "corner",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const aboveTop = anchorRect.y - INLINE_FIELD_HISTORY_BUTTON_SIZE - gap;
|
|
91
|
+
const fitsAbove = viewportHeight <= 0 || aboveTop >= viewportTop + margin;
|
|
92
|
+
return {
|
|
93
|
+
left,
|
|
94
|
+
top: fitsAbove ? aboveTop : anchorRect.y + anchorRect.height + gap,
|
|
95
|
+
showLabel,
|
|
96
|
+
placement: fitsAbove ? "above" : "below",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function formatInlineHistoryEntryLabel(entry, index, total, now = Date.now()) {
|
|
100
|
+
if (!entry)
|
|
101
|
+
return "";
|
|
102
|
+
const who = entry.isBaseline
|
|
103
|
+
? "previous value"
|
|
104
|
+
: (entry.agentId ?? shortenUserName(entry.user));
|
|
105
|
+
const when = formatRelativeHistoryTime(entry.date, now);
|
|
106
|
+
const position = total > 1 ? `${index + 1}/${total}` : "";
|
|
107
|
+
return [when, who, position].filter(Boolean).join(" · ");
|
|
108
|
+
}
|
|
109
|
+
function formatRelativeHistoryTime(date, now) {
|
|
110
|
+
if (!date)
|
|
111
|
+
return "";
|
|
112
|
+
const timestamp = new Date(date).getTime();
|
|
113
|
+
if (!Number.isFinite(timestamp))
|
|
114
|
+
return "";
|
|
115
|
+
const elapsedMs = now - timestamp;
|
|
116
|
+
if (elapsedMs < 45_000)
|
|
117
|
+
return "just now";
|
|
118
|
+
if (elapsedMs >= 6 * 86_400_000)
|
|
119
|
+
return formatDateOnly(new Date(timestamp));
|
|
120
|
+
const units = [
|
|
121
|
+
["day", 86_400_000],
|
|
122
|
+
["hour", 3_600_000],
|
|
123
|
+
["minute", 60_000],
|
|
124
|
+
];
|
|
125
|
+
try {
|
|
126
|
+
const formatter = new Intl.RelativeTimeFormat(undefined, {
|
|
127
|
+
numeric: "auto",
|
|
128
|
+
style: "narrow",
|
|
129
|
+
});
|
|
130
|
+
for (const [unit, unitMs] of units) {
|
|
131
|
+
if (elapsedMs >= unitMs) {
|
|
132
|
+
return formatter.format(-Math.round(elapsedMs / unitMs), unit);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Fall through to the absolute date below.
|
|
138
|
+
}
|
|
139
|
+
return formatDateOnly(new Date(timestamp));
|
|
140
|
+
}
|
|
141
|
+
function idsMatch(left, right) {
|
|
142
|
+
if (!left || !right)
|
|
143
|
+
return false;
|
|
144
|
+
return normalizeMarkerId(left) === normalizeMarkerId(right);
|
|
145
|
+
}
|
|
146
|
+
function toItemDescriptor(item, fallback) {
|
|
147
|
+
const id = item?.id ?? fallback?.id;
|
|
148
|
+
const language = item?.language ?? fallback?.language;
|
|
149
|
+
const version = item?.version ?? fallback?.version;
|
|
150
|
+
if (!id || !language || version == null)
|
|
151
|
+
return undefined;
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
language,
|
|
155
|
+
version,
|
|
156
|
+
...(item?.database || fallback?.database
|
|
157
|
+
? { database: item?.database ?? fallback?.database }
|
|
158
|
+
: {}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export function resolveInlineFieldDescriptor(pageViewContext, focusedField) {
|
|
162
|
+
const activeField = pageViewContext.bridgeSelection?.activeField;
|
|
163
|
+
if (!activeField?.inlineEditing || !activeField.fieldId)
|
|
164
|
+
return undefined;
|
|
165
|
+
const structureFields = pageViewContext.bridgeStructure?.fields ?? [];
|
|
166
|
+
const structureField = structureFields.find((field) => !!activeField.elementKey && field.elementKey === activeField.elementKey) ??
|
|
167
|
+
structureFields.find((field) => idsMatch(field.fieldId, activeField.fieldId) &&
|
|
168
|
+
(!activeField.item?.id ||
|
|
169
|
+
idsMatch(field.item?.id, activeField.item.id)));
|
|
170
|
+
const focusedFieldMatches = focusedField &&
|
|
171
|
+
idsMatch(focusedField.fieldId, activeField.fieldId) &&
|
|
172
|
+
(!activeField.item?.id ||
|
|
173
|
+
idsMatch(focusedField.item.id, activeField.item.id))
|
|
174
|
+
? focusedField
|
|
175
|
+
: undefined;
|
|
176
|
+
const pageItem = pageViewContext.page?.item ?? pageViewContext.pageItemDescriptor;
|
|
177
|
+
const item = toItemDescriptor(structureField?.item ?? activeField.item, focusedFieldMatches?.item ?? pageItem);
|
|
178
|
+
if (!item)
|
|
179
|
+
return undefined;
|
|
180
|
+
return {
|
|
181
|
+
fieldId: structureField?.fieldId ?? activeField.fieldId,
|
|
182
|
+
item,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function fieldDescriptorKey(field) {
|
|
186
|
+
if (!field)
|
|
187
|
+
return "";
|
|
188
|
+
return [
|
|
189
|
+
normalizeMarkerId(field.fieldId),
|
|
190
|
+
normalizeMarkerId(field.item.id),
|
|
191
|
+
field.item.language,
|
|
192
|
+
field.item.version,
|
|
193
|
+
field.item.database ?? "",
|
|
194
|
+
].join(":");
|
|
195
|
+
}
|
|
196
|
+
function anchorMeasurementsEqual(previous, next) {
|
|
197
|
+
return (rectEquals(previous.field, next.field) &&
|
|
198
|
+
previous.viewportLeft === next.viewportLeft &&
|
|
199
|
+
previous.viewportTop === next.viewportTop &&
|
|
200
|
+
previous.viewportWidth === next.viewportWidth &&
|
|
201
|
+
previous.viewportHeight === next.viewportHeight);
|
|
202
|
+
}
|
|
203
|
+
export function InlineFieldHistoryButton({ pageViewContext, field, fieldRect: suppliedFieldRect, testId = "inline-field-history-button", onOpenChange, onPointerPresenceChange, }) {
|
|
204
|
+
const editContext = useEditContext();
|
|
205
|
+
const fieldsContext = useFieldsEditContext();
|
|
206
|
+
const [open, setOpen] = useState(false);
|
|
207
|
+
// The pill idles as a single subtle trigger; the step controls and label
|
|
208
|
+
// only unfold once the user asks for them. The choice is remembered per
|
|
209
|
+
// field while it stays active.
|
|
210
|
+
const [expanded, setExpanded] = useState(false);
|
|
211
|
+
// pageViewContext keeps a stable identity and is mutated in place, so a
|
|
212
|
+
// memoized ancestor can skip re-rendering this overlay when only the
|
|
213
|
+
// selection changed — the pill then never learns a field was focused and
|
|
214
|
+
// appears unreliably (or not at all). Watch the bridge's own messages and
|
|
215
|
+
// re-render directly; the freshly mutated context is read on that render.
|
|
216
|
+
const [, bumpBridgeRevision] = useReducer((n) => n + 1, 0);
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (typeof window === "undefined")
|
|
219
|
+
return;
|
|
220
|
+
const handleBridgeMessage = (event) => {
|
|
221
|
+
const name = event.data?.name;
|
|
222
|
+
if (name === "selectionChanged" ||
|
|
223
|
+
name === "structureUpdated" ||
|
|
224
|
+
name === "inlineEditEnded") {
|
|
225
|
+
bumpBridgeRevision();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
window.addEventListener("message", handleBridgeMessage);
|
|
229
|
+
return () => window.removeEventListener("message", handleBridgeMessage);
|
|
230
|
+
}, []);
|
|
231
|
+
const [resolvedField, setResolvedField] = useState();
|
|
232
|
+
const [historyField, setHistoryField] = useState();
|
|
233
|
+
const [historyDescriptor, setHistoryDescriptor] = useState();
|
|
234
|
+
const [selectedHistoryEntry, setSelectedHistoryEntry] = useState();
|
|
235
|
+
const [quickHistory, setQuickHistory] = useState();
|
|
236
|
+
const [quickHistoryLoading, setQuickHistoryLoading] = useState(false);
|
|
237
|
+
const [quickHistoryIndex, setQuickHistoryIndex] = useState(0);
|
|
238
|
+
const [quickHistoryReloadToken, setQuickHistoryReloadToken] = useState(0);
|
|
239
|
+
const [controlsWidth, setControlsWidth] = useState(0);
|
|
240
|
+
const [labelWidth, setLabelWidth] = useState(0);
|
|
241
|
+
const pillRef = useRef(null);
|
|
242
|
+
const controlsRef = useRef(null);
|
|
243
|
+
const labelRef = useRef(null);
|
|
244
|
+
const inlineDescriptor = useMemo(() => resolveInlineFieldDescriptor(pageViewContext, fieldsContext?.focusedField), [
|
|
245
|
+
fieldsContext?.focusedField,
|
|
246
|
+
pageViewContext.bridgeSelection,
|
|
247
|
+
pageViewContext.bridgeStructure,
|
|
248
|
+
pageViewContext.page,
|
|
249
|
+
pageViewContext.pageItemDescriptor,
|
|
250
|
+
]);
|
|
251
|
+
const activeDescriptor = field?.descriptor ?? inlineDescriptor;
|
|
252
|
+
const activeDescriptorKey = fieldDescriptorKey(activeDescriptor);
|
|
253
|
+
const activeFieldId = activeDescriptor?.fieldId;
|
|
254
|
+
const activeItemId = activeDescriptor?.item.id;
|
|
255
|
+
const activeItemLanguage = activeDescriptor?.item.language;
|
|
256
|
+
const activeItemVersion = activeDescriptor?.item.version;
|
|
257
|
+
const activeItemDatabase = activeDescriptor?.item.database;
|
|
258
|
+
const targetDescriptor = open ? historyDescriptor : activeDescriptor;
|
|
259
|
+
const targetDescriptorKey = fieldDescriptorKey(targetDescriptor);
|
|
260
|
+
// The edit context and the resolved descriptors are rebuilt on virtually every
|
|
261
|
+
// editor state change (each save bumps the items-repository revision, each
|
|
262
|
+
// caret move produces a fresh bridge selection). Depending on their identity
|
|
263
|
+
// re-ran the fetches below on every keystroke. Read them through refs and key
|
|
264
|
+
// the effects on the stable descriptor key instead.
|
|
265
|
+
const editContextRef = useRef(editContext);
|
|
266
|
+
editContextRef.current = editContext;
|
|
267
|
+
const targetDescriptorRef = useRef(targetDescriptor);
|
|
268
|
+
targetDescriptorRef.current = targetDescriptor;
|
|
269
|
+
const hasEditContext = !!editContext;
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
if (!hasEditContext ||
|
|
272
|
+
!activeDescriptorKey ||
|
|
273
|
+
!activeFieldId ||
|
|
274
|
+
!activeItemId ||
|
|
275
|
+
!activeItemLanguage ||
|
|
276
|
+
activeItemVersion == null) {
|
|
277
|
+
setQuickHistory(undefined);
|
|
278
|
+
setQuickHistoryLoading(false);
|
|
279
|
+
setQuickHistoryIndex(0);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
let cancelled = false;
|
|
283
|
+
const existingSession = getQuickHistorySession(activeDescriptorKey);
|
|
284
|
+
if (existingSession) {
|
|
285
|
+
setQuickHistory({
|
|
286
|
+
key: activeDescriptorKey,
|
|
287
|
+
entries: existingSession.entries,
|
|
288
|
+
fetchedAt: Date.now(),
|
|
289
|
+
});
|
|
290
|
+
setQuickHistoryIndex(existingSession.index);
|
|
291
|
+
setQuickHistoryLoading(false);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
setQuickHistory((current) => current?.key === activeDescriptorKey ? current : undefined);
|
|
295
|
+
setQuickHistoryLoading(true);
|
|
296
|
+
void getFieldHistory({
|
|
297
|
+
id: activeItemId,
|
|
298
|
+
language: activeItemLanguage,
|
|
299
|
+
version: activeItemVersion,
|
|
300
|
+
...(activeItemDatabase ? { database: activeItemDatabase } : {}),
|
|
301
|
+
}, activeFieldId)
|
|
302
|
+
.then((result) => {
|
|
303
|
+
if (cancelled)
|
|
304
|
+
return;
|
|
305
|
+
setQuickHistory({
|
|
306
|
+
key: activeDescriptorKey,
|
|
307
|
+
entries: collapseFieldHistoryEntries(result.data ?? []),
|
|
308
|
+
fetchedAt: Date.now(),
|
|
309
|
+
});
|
|
310
|
+
setQuickHistoryLoading(false);
|
|
311
|
+
setQuickHistoryIndex(0);
|
|
312
|
+
})
|
|
313
|
+
.catch(() => {
|
|
314
|
+
if (cancelled)
|
|
315
|
+
return;
|
|
316
|
+
setQuickHistory({
|
|
317
|
+
key: activeDescriptorKey,
|
|
318
|
+
entries: [],
|
|
319
|
+
fetchedAt: Date.now(),
|
|
320
|
+
});
|
|
321
|
+
setQuickHistoryLoading(false);
|
|
322
|
+
});
|
|
323
|
+
return () => {
|
|
324
|
+
cancelled = true;
|
|
325
|
+
};
|
|
326
|
+
}, [
|
|
327
|
+
activeDescriptorKey,
|
|
328
|
+
activeFieldId,
|
|
329
|
+
activeItemDatabase,
|
|
330
|
+
activeItemId,
|
|
331
|
+
activeItemLanguage,
|
|
332
|
+
activeItemVersion,
|
|
333
|
+
hasEditContext,
|
|
334
|
+
quickHistoryReloadToken,
|
|
335
|
+
]);
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
const currentEditContext = editContextRef.current;
|
|
338
|
+
const descriptor = targetDescriptorRef.current;
|
|
339
|
+
if (!currentEditContext || !descriptor) {
|
|
340
|
+
if (!descriptor)
|
|
341
|
+
setResolvedField(undefined);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (field && fieldDescriptorKey(field.descriptor) === targetDescriptorKey) {
|
|
345
|
+
setResolvedField({ key: targetDescriptorKey, field });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
let cancelled = false;
|
|
349
|
+
setResolvedField((current) => current?.key === targetDescriptorKey ? current : undefined);
|
|
350
|
+
void currentEditContext.itemsRepository
|
|
351
|
+
.getField(descriptor)
|
|
352
|
+
.then((resolved) => {
|
|
353
|
+
if (!cancelled && resolved) {
|
|
354
|
+
setResolvedField({ key: targetDescriptorKey, field: resolved });
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
return () => {
|
|
358
|
+
cancelled = true;
|
|
359
|
+
};
|
|
360
|
+
}, [hasEditContext, field, targetDescriptorKey]);
|
|
361
|
+
const activeField = resolvedField?.key === activeDescriptorKey
|
|
362
|
+
? resolvedField.field
|
|
363
|
+
: undefined;
|
|
364
|
+
const displayedField = open && resolvedField?.key === targetDescriptorKey
|
|
365
|
+
? resolvedField.field
|
|
366
|
+
: open
|
|
367
|
+
? historyField
|
|
368
|
+
: activeField;
|
|
369
|
+
const measuredDescriptor = targetDescriptor;
|
|
370
|
+
const bridgeActiveField = pageViewContext.bridgeSelection?.activeField;
|
|
371
|
+
const bridgeElementKey = activeDescriptor &&
|
|
372
|
+
idsMatch(bridgeActiveField?.fieldId, activeDescriptor.fieldId) &&
|
|
373
|
+
(!bridgeActiveField?.item?.id ||
|
|
374
|
+
idsMatch(bridgeActiveField.item.id, activeDescriptor.item.id))
|
|
375
|
+
? bridgeActiveField?.elementKey
|
|
376
|
+
: undefined;
|
|
377
|
+
const liveFieldValue = bridgeElementKey
|
|
378
|
+
? bridgeActiveField?.value
|
|
379
|
+
: undefined;
|
|
380
|
+
// Typing keeps the pill away and schedules one history refresh for when the
|
|
381
|
+
// value settles (a changed value means a save will append a history entry).
|
|
382
|
+
// Only genuine value changes count as activity: caret moves and bridge
|
|
383
|
+
// re-publishes with the same value used to hide the pill for 2s around every
|
|
384
|
+
// click, and transient re-publishes without a matching field (canvas
|
|
385
|
+
// refreshes) made it flap. The timers live in refs so a dependency change
|
|
386
|
+
// that ends up a no-op cannot cancel a pending appearance.
|
|
387
|
+
const [idle, setIdle] = useState(false);
|
|
388
|
+
const idleRef = useRef(idle);
|
|
389
|
+
idleRef.current = idle;
|
|
390
|
+
const selfChangeUntilRef = useRef(0);
|
|
391
|
+
const idleTimerRef = useRef(undefined);
|
|
392
|
+
const reloadTimerRef = useRef(undefined);
|
|
393
|
+
const lastLiveValueRef = useRef(undefined);
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
const restartIdleWait = () => {
|
|
396
|
+
if (idleTimerRef.current)
|
|
397
|
+
clearTimeout(idleTimerRef.current);
|
|
398
|
+
setIdle(false);
|
|
399
|
+
idleTimerRef.current = setTimeout(() => setIdle(true), INLINE_FIELD_HISTORY_IDLE_DELAY_MS);
|
|
400
|
+
};
|
|
401
|
+
if (!activeDescriptorKey) {
|
|
402
|
+
lastLiveValueRef.current = undefined;
|
|
403
|
+
if (idleTimerRef.current)
|
|
404
|
+
clearTimeout(idleTimerRef.current);
|
|
405
|
+
if (reloadTimerRef.current)
|
|
406
|
+
clearTimeout(reloadTimerRef.current);
|
|
407
|
+
setIdle(false);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const last = lastLiveValueRef.current;
|
|
411
|
+
if (last?.key !== activeDescriptorKey) {
|
|
412
|
+
lastLiveValueRef.current = {
|
|
413
|
+
key: activeDescriptorKey,
|
|
414
|
+
...(typeof liveFieldValue === "string"
|
|
415
|
+
? { value: liveFieldValue }
|
|
416
|
+
: {}),
|
|
417
|
+
};
|
|
418
|
+
if (reloadTimerRef.current)
|
|
419
|
+
clearTimeout(reloadTimerRef.current);
|
|
420
|
+
restartIdleWait();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
// A vanished value (the bridge re-publishing without the field, e.g.
|
|
424
|
+
// during a canvas refresh) is not activity; keep the last known value so
|
|
425
|
+
// its return with the same text stays a no-op.
|
|
426
|
+
if (typeof liveFieldValue !== "string")
|
|
427
|
+
return;
|
|
428
|
+
const valueChanged = last.value !== liveFieldValue;
|
|
429
|
+
last.value = liveFieldValue;
|
|
430
|
+
if (!valueChanged)
|
|
431
|
+
return;
|
|
432
|
+
// An in-progress stepping session short-circuits the refetch, so this
|
|
433
|
+
// cannot move the index under the user.
|
|
434
|
+
if (reloadTimerRef.current)
|
|
435
|
+
clearTimeout(reloadTimerRef.current);
|
|
436
|
+
reloadTimerRef.current = setTimeout(() => {
|
|
437
|
+
reloadTimerRef.current = undefined;
|
|
438
|
+
setQuickHistoryReloadToken((token) => token + 1);
|
|
439
|
+
}, QUICK_HISTORY_REFRESH_DELAY_MS);
|
|
440
|
+
// Our own step echoing back from the canvas is not the user typing; it
|
|
441
|
+
// must not hide the pill mid-step.
|
|
442
|
+
if (Date.now() < selfChangeUntilRef.current && idleRef.current)
|
|
443
|
+
return;
|
|
444
|
+
restartIdleWait();
|
|
445
|
+
}, [activeDescriptorKey, liveFieldValue]);
|
|
446
|
+
useEffect(() => () => {
|
|
447
|
+
if (idleTimerRef.current)
|
|
448
|
+
clearTimeout(idleTimerRef.current);
|
|
449
|
+
if (reloadTimerRef.current)
|
|
450
|
+
clearTimeout(reloadTimerRef.current);
|
|
451
|
+
}, []);
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
setExpanded(false);
|
|
454
|
+
}, [activeDescriptorKey]);
|
|
455
|
+
// Text fields anchor to the field block's own bounds — its top-left corner
|
|
456
|
+
// stays put while values change, so stepping never moves the buttons under
|
|
457
|
+
// the pointer. Media fields keep the supplied bounds.
|
|
458
|
+
const anchorsToText = !suppliedFieldRect && !!measuredDescriptor;
|
|
459
|
+
const measurement = useOverlayMeasurement((metrics) => {
|
|
460
|
+
const fieldRect = suppliedFieldRect ??
|
|
461
|
+
(measuredDescriptor
|
|
462
|
+
? measureBridgeFieldRect(pageViewContext.bridgeGeometry, pageViewContext.bridgeStructure, measuredDescriptor, metrics)
|
|
463
|
+
: null);
|
|
464
|
+
return {
|
|
465
|
+
field: fieldRect,
|
|
466
|
+
viewportLeft: metrics.scrollLeft,
|
|
467
|
+
viewportTop: metrics.scrollTop,
|
|
468
|
+
viewportWidth: metrics.viewportWidth,
|
|
469
|
+
viewportHeight: metrics.viewportHeight,
|
|
470
|
+
};
|
|
471
|
+
}, [
|
|
472
|
+
measuredDescriptor,
|
|
473
|
+
pageViewContext.bridgeGeometry,
|
|
474
|
+
pageViewContext.bridgeStructure,
|
|
475
|
+
suppliedFieldRect,
|
|
476
|
+
], {
|
|
477
|
+
initialValue: {
|
|
478
|
+
field: null,
|
|
479
|
+
viewportLeft: 0,
|
|
480
|
+
viewportTop: 0,
|
|
481
|
+
viewportWidth: 0,
|
|
482
|
+
viewportHeight: 0,
|
|
483
|
+
},
|
|
484
|
+
equals: anchorMeasurementsEqual,
|
|
485
|
+
interests: ["viewport", "layout", "text", "adornment-data"],
|
|
486
|
+
});
|
|
487
|
+
const quickHistoryEntries = quickHistory?.key === activeDescriptorKey ? quickHistory.entries : [];
|
|
488
|
+
const boundedIndex = Math.max(0, Math.min(quickHistoryIndex, quickHistoryEntries.length - 1));
|
|
489
|
+
const pendingApplyRef = useRef(undefined);
|
|
490
|
+
const applyTimerRef = useRef(undefined);
|
|
491
|
+
const flushPendingApply = async () => {
|
|
492
|
+
const pending = pendingApplyRef.current;
|
|
493
|
+
if (applyTimerRef.current) {
|
|
494
|
+
clearTimeout(applyTimerRef.current);
|
|
495
|
+
applyTimerRef.current = undefined;
|
|
496
|
+
}
|
|
497
|
+
if (!pending)
|
|
498
|
+
return;
|
|
499
|
+
pendingApplyRef.current = undefined;
|
|
500
|
+
const currentEditContext = editContextRef.current;
|
|
501
|
+
if (!currentEditContext)
|
|
502
|
+
return;
|
|
503
|
+
await currentEditContext.operations.editField({
|
|
504
|
+
field: pending.descriptor,
|
|
505
|
+
rawValue: pending.entry.rawValue ?? null,
|
|
506
|
+
// A canvas that was already patched shows the stepped value; forcing a
|
|
507
|
+
// refresh here tears down the inline-edit session (dropping selection
|
|
508
|
+
// and the pill with it). Only media fields, which take no patch, need
|
|
509
|
+
// the re-render to show the restored value.
|
|
510
|
+
refresh: pending.refresh,
|
|
511
|
+
});
|
|
512
|
+
};
|
|
513
|
+
const flushPendingApplyRef = useRef(flushPendingApply);
|
|
514
|
+
flushPendingApplyRef.current = flushPendingApply;
|
|
515
|
+
// A step that is still coalescing when the field is left (or the editor
|
|
516
|
+
// unmounts) must not be dropped — flush it instead.
|
|
517
|
+
useEffect(() => () => {
|
|
518
|
+
void flushPendingApplyRef.current();
|
|
519
|
+
}, [activeDescriptorKey]);
|
|
520
|
+
const applyQuickHistoryIndex = (index) => {
|
|
521
|
+
const nextIndex = Math.max(0, Math.min(index, quickHistoryEntries.length - 1));
|
|
522
|
+
const nextEntry = quickHistoryEntries[nextIndex];
|
|
523
|
+
if (!nextEntry || !activeDescriptor || !editContext)
|
|
524
|
+
return;
|
|
525
|
+
setQuickHistoryIndex(nextIndex);
|
|
526
|
+
setSelectedHistoryEntry(nextEntry);
|
|
527
|
+
selfChangeUntilRef.current =
|
|
528
|
+
Date.now() + INLINE_FIELD_HISTORY_SELF_CHANGE_WINDOW_MS;
|
|
529
|
+
quickHistorySessions.set(activeDescriptorKey, {
|
|
530
|
+
entries: quickHistoryEntries,
|
|
531
|
+
index: nextIndex,
|
|
532
|
+
expiresAt: Date.now() + QUICK_HISTORY_SESSION_TTL_MS,
|
|
533
|
+
});
|
|
534
|
+
const displayField = activeField ?? field;
|
|
535
|
+
const structureField = pageViewContext.bridgeStructure?.fields.find((candidate) => idsMatch(candidate.fieldId, activeDescriptor.fieldId) &&
|
|
536
|
+
(!candidate.item?.id ||
|
|
537
|
+
idsMatch(candidate.item.id, activeDescriptor.item.id)));
|
|
538
|
+
const elementKey = bridgeElementKey ?? structureField?.elementKey;
|
|
539
|
+
const fieldType = displayField?.type.toLowerCase();
|
|
540
|
+
// Show the value straight away; the save below is coalesced so holding the
|
|
541
|
+
// arrow down does not queue one round-trip per step.
|
|
542
|
+
const canPatchCanvas = !!elementKey && fieldType !== "image" && fieldType !== "picture";
|
|
543
|
+
if (canPatchCanvas) {
|
|
544
|
+
pageViewContext.requestBridgeFieldPatch?.({
|
|
545
|
+
elementKey,
|
|
546
|
+
fieldId: activeDescriptor.fieldId,
|
|
547
|
+
value: nextEntry.rawValue ?? "",
|
|
548
|
+
isRichText: structureField?.isRichText === true || fieldType === "rich text",
|
|
549
|
+
source: "reset",
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
pendingApplyRef.current = {
|
|
553
|
+
descriptor: activeDescriptor,
|
|
554
|
+
entry: nextEntry,
|
|
555
|
+
refresh: canPatchCanvas ? "none" : "immediate",
|
|
556
|
+
};
|
|
557
|
+
if (applyTimerRef.current)
|
|
558
|
+
clearTimeout(applyTimerRef.current);
|
|
559
|
+
applyTimerRef.current = setTimeout(() => {
|
|
560
|
+
applyTimerRef.current = undefined;
|
|
561
|
+
flushPendingApplyRef.current();
|
|
562
|
+
}, QUICK_HISTORY_APPLY_DEBOUNCE_MS);
|
|
563
|
+
};
|
|
564
|
+
const applyQuickHistoryIndexRef = useRef(applyQuickHistoryIndex);
|
|
565
|
+
applyQuickHistoryIndexRef.current = applyQuickHistoryIndex;
|
|
566
|
+
const canStepOlder = boundedIndex < quickHistoryEntries.length - 1;
|
|
567
|
+
const canStepNewer = boundedIndex > 0;
|
|
568
|
+
const stepEnabled = !open && quickHistoryEntries.length > 1;
|
|
569
|
+
const canStepOlderRef = useRef(canStepOlder);
|
|
570
|
+
canStepOlderRef.current = canStepOlder && stepEnabled;
|
|
571
|
+
const canStepNewerRef = useRef(canStepNewer);
|
|
572
|
+
canStepNewerRef.current = canStepNewer && stepEnabled;
|
|
573
|
+
const boundedIndexRef = useRef(boundedIndex);
|
|
574
|
+
boundedIndexRef.current = boundedIndex;
|
|
575
|
+
// Alt+Up / Alt+Down while the caret is in the field. Canvas keystrokes reach
|
|
576
|
+
// the parent as a bridge interaction that PageViewerFrame republishes as this
|
|
577
|
+
// event, so one listener covers both the canvas and the side panel.
|
|
578
|
+
useEffect(() => {
|
|
579
|
+
if (typeof document === "undefined")
|
|
580
|
+
return;
|
|
581
|
+
const handleStep = (event) => {
|
|
582
|
+
const direction = event.detail
|
|
583
|
+
?.direction;
|
|
584
|
+
if (direction === "older" && canStepOlderRef.current) {
|
|
585
|
+
applyQuickHistoryIndexRef.current(boundedIndexRef.current + 1);
|
|
586
|
+
}
|
|
587
|
+
else if (direction === "newer" && canStepNewerRef.current) {
|
|
588
|
+
applyQuickHistoryIndexRef.current(boundedIndexRef.current - 1);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
// Stepping from the keyboard unfolds the controls so the position label
|
|
594
|
+
// gives feedback on where in the history the field is.
|
|
595
|
+
setExpanded(true);
|
|
596
|
+
};
|
|
597
|
+
document.addEventListener(INLINE_FIELD_HISTORY_STEP_EVENT, handleStep);
|
|
598
|
+
return () => document.removeEventListener(INLINE_FIELD_HISTORY_STEP_EVENT, handleStep);
|
|
599
|
+
}, []);
|
|
600
|
+
const anchorRect = measurement.field;
|
|
601
|
+
// Both widths are remembered rather than read live: the label is measured
|
|
602
|
+
// while it is shown and its remembered width decides whether it may come back,
|
|
603
|
+
// so hiding it cannot make it immediately fit and flap. Measuring after every
|
|
604
|
+
// render is deliberate — the label's text (and width) changes as you step —
|
|
605
|
+
// and the thresholds below stop the state updates from chaining.
|
|
606
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
607
|
+
useLayoutEffect(() => {
|
|
608
|
+
const controls = controlsRef.current?.offsetWidth ?? 0;
|
|
609
|
+
if (controls > 0 && Math.abs(controls - controlsWidth) > 1) {
|
|
610
|
+
setControlsWidth(controls);
|
|
611
|
+
}
|
|
612
|
+
const label = labelRef.current?.offsetWidth ?? 0;
|
|
613
|
+
if (label > 0 && Math.abs(label - labelWidth) > 1) {
|
|
614
|
+
setLabelWidth(label);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
if (!editContext ||
|
|
618
|
+
editContext.readonly ||
|
|
619
|
+
editContext.mode !== "edit" ||
|
|
620
|
+
!measuredDescriptor ||
|
|
621
|
+
!anchorRect ||
|
|
622
|
+
// Text fields hold the pill back until typing settles; an open popover
|
|
623
|
+
// outlives the idle window it was opened from. Media fields are not typed
|
|
624
|
+
// into and their pill is already gated by the hover overlay that owns it.
|
|
625
|
+
(anchorsToText && !idle && !open)) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
// Media pills collapse too: same subtle trigger, same unfold-on-click.
|
|
629
|
+
const collapsedTrigger = !expanded && !open;
|
|
630
|
+
const position = calculateInlineFieldHistoryButtonPosition(anchorRect, {
|
|
631
|
+
anchor: anchorsToText ? "block" : "corner",
|
|
632
|
+
controlsWidth,
|
|
633
|
+
labelWidth: collapsedTrigger ? 0 : labelWidth,
|
|
634
|
+
viewportLeft: measurement.viewportLeft,
|
|
635
|
+
viewportTop: measurement.viewportTop,
|
|
636
|
+
viewportWidth: measurement.viewportWidth,
|
|
637
|
+
viewportHeight: measurement.viewportHeight,
|
|
638
|
+
});
|
|
639
|
+
const entryLabel = formatInlineHistoryEntryLabel(quickHistoryEntries[boundedIndex], boundedIndex, quickHistoryEntries.length);
|
|
640
|
+
const showStepControls = !open && !collapsedTrigger && quickHistoryEntries.length > 0;
|
|
641
|
+
const showLabel = showStepControls &&
|
|
642
|
+
position.showLabel &&
|
|
643
|
+
(!!entryLabel || quickHistoryLoading);
|
|
644
|
+
const closeHistory = () => {
|
|
645
|
+
setOpen(false);
|
|
646
|
+
setSelectedHistoryEntry(undefined);
|
|
647
|
+
setHistoryField(undefined);
|
|
648
|
+
setHistoryDescriptor(undefined);
|
|
649
|
+
onOpenChange?.(false);
|
|
650
|
+
};
|
|
651
|
+
return (_jsxs("div", { ref: pillRef, className: "animate-in fade-in pointer-events-auto absolute flex items-center gap-1.5 duration-200", style: {
|
|
652
|
+
left: position.left,
|
|
653
|
+
top: position.top,
|
|
654
|
+
minHeight: INLINE_FIELD_HISTORY_BUTTON_SIZE,
|
|
655
|
+
zIndex: 900,
|
|
656
|
+
}, "data-field-history-trigger": "true", "data-testid": `${testId}-container`, onMouseDown: (event) => {
|
|
657
|
+
// The pill floats over the live canvas; a click that slips past a
|
|
658
|
+
// button must neither blur the iframe's edit session nor fall
|
|
659
|
+
// through as a canvas interaction.
|
|
660
|
+
event.preventDefault();
|
|
661
|
+
event.stopPropagation();
|
|
662
|
+
}, onPointerEnter: () => {
|
|
663
|
+
onPointerPresenceChange?.(true);
|
|
664
|
+
// Refresh stale data on approach, but never while a fetch is running
|
|
665
|
+
// and never for fresh data: an unconditional refetch here looped — the
|
|
666
|
+
// loading pill shrank out from under the cursor and re-entered when
|
|
667
|
+
// the response grew it back.
|
|
668
|
+
if (!getQuickHistorySession(activeDescriptorKey) &&
|
|
669
|
+
!quickHistoryLoading &&
|
|
670
|
+
quickHistory?.key === activeDescriptorKey &&
|
|
671
|
+
Date.now() - quickHistory.fetchedAt > QUICK_HISTORY_HOVER_STALE_MS) {
|
|
672
|
+
setQuickHistoryReloadToken((token) => token + 1);
|
|
673
|
+
}
|
|
674
|
+
}, onPointerLeave: () => onPointerPresenceChange?.(false), onMouseEnter: () => onPointerPresenceChange?.(true), onMouseLeave: () => onPointerPresenceChange?.(false), children: [_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute -inset-2" }), collapsedTrigger ? (_jsx("div", { ref: controlsRef, className: "relative flex h-6 items-center", children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "border-border-default/60 text-neutral-grey-100 hover:border-border-default flex h-6 w-6 cursor-pointer items-center justify-center rounded-[4px] border bg-white/70 opacity-70 hover:bg-white hover:opacity-100 hover:shadow-sm", "aria-label": "Field History", "aria-expanded": false, "data-field-history-trigger": "true", "data-testid": testId, onMouseDown: (event) => {
|
|
675
|
+
// Keep the iframe's active field alive while unfolding.
|
|
676
|
+
event.preventDefault();
|
|
677
|
+
event.stopPropagation();
|
|
678
|
+
}, onClick: (event) => {
|
|
679
|
+
event.preventDefault();
|
|
680
|
+
event.stopPropagation();
|
|
681
|
+
setExpanded(true);
|
|
682
|
+
// The pill can mount while an edit is still being saved. Its
|
|
683
|
+
// eager request then contains no previous value, and the
|
|
684
|
+
// value-change effect cannot detect that save if the edited
|
|
685
|
+
// value was already present on mount. Opening the controls is
|
|
686
|
+
// an explicit request for current history, so refresh unless
|
|
687
|
+
// the user already has a quick-navigation session whose
|
|
688
|
+
// position must be preserved.
|
|
689
|
+
if (!getQuickHistorySession(activeDescriptorKey)) {
|
|
690
|
+
setQuickHistoryReloadToken((token) => token + 1);
|
|
691
|
+
}
|
|
692
|
+
}, children: _jsx(History, { "aria-hidden": "true", strokeWidth: 1, className: "h-3.5 w-3.5" }) }) }), _jsx(TooltipContent, { side: "top", children: "Field History" })] }) })) : (_jsxs("div", { ref: controlsRef, className: "border-border-default relative flex h-6 items-center overflow-hidden rounded-[4px] border bg-white shadow-sm", children: [showStepControls && (_jsxs(_Fragment, { children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "hover:bg-neutral-grey-5 text-neutral-grey-100 flex h-full w-[22px] cursor-pointer items-center justify-center disabled:cursor-not-allowed disabled:opacity-35", "aria-label": "Older history value", "data-testid": "field-history-step-older", disabled: !canStepOlder, onMouseDown: (event) => event.preventDefault(), onClick: (event) => {
|
|
693
|
+
event.preventDefault();
|
|
694
|
+
event.stopPropagation();
|
|
695
|
+
applyQuickHistoryIndex(boundedIndex + 1);
|
|
696
|
+
}, children: _jsx(Undo2, { "aria-hidden": "true", className: "h-3.5 w-3.5", strokeWidth: 1 }) }) }), _jsx(TooltipContent, { side: "top", children: "Older value (Alt+Down)" })] }), _jsx("span", { className: "bg-border-default h-6 w-px" }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "hover:bg-neutral-grey-5 text-neutral-grey-100 flex h-full w-[22px] cursor-pointer items-center justify-center disabled:cursor-not-allowed disabled:opacity-35", "aria-label": "Newer history value", "data-testid": "field-history-step-newer", disabled: !canStepNewer, onMouseDown: (event) => event.preventDefault(), onClick: (event) => {
|
|
697
|
+
event.preventDefault();
|
|
698
|
+
event.stopPropagation();
|
|
699
|
+
applyQuickHistoryIndex(boundedIndex - 1);
|
|
700
|
+
}, children: _jsx(Redo2, { "aria-hidden": "true", className: "h-3.5 w-3.5", strokeWidth: 1 }) }) }), _jsx(TooltipContent, { side: "top", children: "Newer value (Alt+Up)" })] }), _jsx("span", { className: "bg-border-default h-6 w-px" })] })), _jsxs(Tooltip, { children: [_jsxs(Popover, { open: open, onOpenChange: (nextOpen) => {
|
|
701
|
+
if (nextOpen) {
|
|
702
|
+
if (!activeDescriptor)
|
|
703
|
+
return;
|
|
704
|
+
const descriptor = activeDescriptor;
|
|
705
|
+
const requestedField = field ?? activeField;
|
|
706
|
+
void (async () => {
|
|
707
|
+
// Full history must be a persistence boundary. A quick
|
|
708
|
+
// step patches the canvas immediately and saves on a
|
|
709
|
+
// trailing debounce; fetching while that save is pending
|
|
710
|
+
// can preview the value from the preceding step. Flush
|
|
711
|
+
// both this component's coalescing timer and the editor's
|
|
712
|
+
// field-save queue before mounting FieldHistory (whose
|
|
713
|
+
// first effect performs the GET).
|
|
714
|
+
await flushPendingApply();
|
|
715
|
+
await editContext.operations.onFieldBlur();
|
|
716
|
+
setHistoryDescriptor(descriptor);
|
|
717
|
+
setHistoryField(requestedField);
|
|
718
|
+
setOpen(true);
|
|
719
|
+
onOpenChange?.(true);
|
|
720
|
+
})();
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
closeHistory();
|
|
724
|
+
}
|
|
725
|
+
}, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "hover:bg-neutral-grey-5 text-neutral-grey-100 flex h-full w-6 shrink-0 cursor-pointer items-center justify-center", "aria-label": "Field History", "data-field-history-trigger": "true", "data-testid": testId, onMouseDown: (event) => {
|
|
726
|
+
// Keep the iframe's active field alive while the trigger opens.
|
|
727
|
+
event.preventDefault();
|
|
728
|
+
event.stopPropagation();
|
|
729
|
+
}, onClick: (event) => event.stopPropagation(), children: _jsx(History, { "aria-hidden": "true", strokeWidth: 1, className: "h-3.5 w-3.5" }) }) }) }), _jsx(PopoverContent, { className: "w-[700px] overflow-hidden p-0", align: "end", side: "left", sideOffset: 8, children: displayedField ? (_jsx(FieldHistory, { field: displayedField, selectedEntry: selectedHistoryEntry, onSelectedEntryChange: setSelectedHistoryEntry, configuration: editContext.configuration, showRawValue: false, onRevert: closeHistory, onClose: closeHistory })) : (_jsx("div", { className: "text-neutral-grey-50 flex h-[500px] items-center justify-center text-sm", "data-testid": "field-history-loading", children: "Loading field history\u2026" })) })] }), _jsx(TooltipContent, { side: "top", children: "Field History" })] })] })), showLabel && (_jsx("div", { ref: labelRef, className: "border-border-default text-neutral-grey-50 relative flex h-6 items-center gap-1 overflow-hidden rounded-[4px] border bg-white px-2 text-[10px] whitespace-nowrap shadow-sm", style: { maxWidth: QUICK_HISTORY_LABEL_MAX_WIDTH }, "data-testid": "field-history-entry-label", title: entryLabel, children: quickHistoryLoading && !entryLabel ? (_jsx(Loader2, { "aria-label": "Loading field history", className: "h-3 w-3 animate-spin", strokeWidth: 1 })) : (_jsx("span", { className: "truncate", children: entryLabel })) }))] }));
|
|
730
|
+
}
|
|
731
|
+
//# sourceMappingURL=InlineFieldHistoryButton.js.map
|