@gogitcms/editor 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/src/App.tsx +60 -1
- package/app/src/previewHost.tsx +82 -2
- package/app/vendor/design-system/src/__tests__/ContentBrowser.fieldfocus.test.tsx +109 -0
- package/app/vendor/design-system/src/components/ContentBrowser.tsx +120 -5
- package/app/vendor/plugin-sdk/src/index.ts +1 -0
- package/app/vendor/plugin-sdk/src/types.ts +26 -0
- package/npm-shrinkwrap.json +34 -34
- package/package.json +6 -6
package/app/src/App.tsx
CHANGED
|
@@ -33,17 +33,19 @@ import {
|
|
|
33
33
|
pluginNavKey,
|
|
34
34
|
usePluginRegistry,
|
|
35
35
|
type PluginDocument,
|
|
36
|
+
type PluginFieldFocus,
|
|
36
37
|
type PluginTabSpec,
|
|
37
38
|
} from "@gogitcms/plugin-sdk";
|
|
38
39
|
import { pluginRegistry } from "./plugins";
|
|
39
40
|
import {
|
|
40
41
|
DraftBus,
|
|
42
|
+
FocusBus,
|
|
41
43
|
PluginDocumentActions,
|
|
42
44
|
PluginTabBody,
|
|
43
45
|
pluginTabId,
|
|
44
46
|
type PluginTabRef,
|
|
45
47
|
} from "./previewHost";
|
|
46
|
-
import { useDocCollab, type DocCollab } from "@gogitcms/realtime";
|
|
48
|
+
import { colorForId, useDocCollab, type DocCollab } from "@gogitcms/realtime";
|
|
47
49
|
import { client, setAuthToken, getAuthToken } from "./apollo";
|
|
48
50
|
import { config, analyticsConfig } from "./config";
|
|
49
51
|
import { AnalyticsEvent, AnalyticsProvider, useAnalytics, useIdentify } from "@gogitcms/analytics";
|
|
@@ -1388,6 +1390,8 @@ function CmsView({
|
|
|
1388
1390
|
// One per browser instance, never recreated — a new bus would silently drop
|
|
1389
1391
|
// every existing subscription.
|
|
1390
1392
|
const draftBus = useRef(new DraftBus()).current;
|
|
1393
|
+
// Who is focused on which field of each open document, for the same tabs.
|
|
1394
|
+
const focusBus = useRef(new FocusBus()).current;
|
|
1391
1395
|
|
|
1392
1396
|
// Current user identity for collaborative presence (email local-part as label).
|
|
1393
1397
|
const { data: meData } = useQuery(ME);
|
|
@@ -1411,6 +1415,50 @@ function CmsView({
|
|
|
1411
1415
|
return next;
|
|
1412
1416
|
});
|
|
1413
1417
|
}, []);
|
|
1418
|
+
// Field focus, merged per document from two sources: the field this editor's
|
|
1419
|
+
// author is in (ContentBrowser's onFieldFocus) and the fields collaborators
|
|
1420
|
+
// are in (the document's awareness, the same presence the form shows as
|
|
1421
|
+
// coloured outlines). Refs rather than state: both move on every focus
|
|
1422
|
+
// change and awareness tick, and nothing here re-renders on them — the bus
|
|
1423
|
+
// fans the merged set straight out to the preview panes.
|
|
1424
|
+
const localFocusRef = useRef<Record<string, string | null>>({});
|
|
1425
|
+
const loadedRef = useRef(loaded);
|
|
1426
|
+
loadedRef.current = loaded;
|
|
1427
|
+
const collabRef = useRef(collabById);
|
|
1428
|
+
collabRef.current = collabById;
|
|
1429
|
+
const collabUserRef = useRef(collabUser);
|
|
1430
|
+
collabUserRef.current = collabUser;
|
|
1431
|
+
const publishFocus = useCallback(
|
|
1432
|
+
(id: string) => {
|
|
1433
|
+
const path = loadedRef.current[id]?.path ?? "";
|
|
1434
|
+
const me = collabUserRef.current;
|
|
1435
|
+
const focus: PluginFieldFocus[] = [];
|
|
1436
|
+
const local = localFocusRef.current[id];
|
|
1437
|
+
// The author's own colour is the one collab derives for them, so the
|
|
1438
|
+
// ring on the page matches the outline a collaborator sees in the form.
|
|
1439
|
+
if (local && me) {
|
|
1440
|
+
focus.push({ documentId: id, path, field: local, user: { id: me.id, name: me.name, color: colorForId(me.id) }, self: true });
|
|
1441
|
+
}
|
|
1442
|
+
const collab = collabRef.current[id];
|
|
1443
|
+
if (collab) {
|
|
1444
|
+
for (const p of collab.participants()) {
|
|
1445
|
+
if (!p.focus) continue;
|
|
1446
|
+
focus.push({ documentId: id, path, field: p.focus, user: { id: p.id, name: p.name, color: p.color }, self: false });
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
focusBus.publish(id, focus);
|
|
1450
|
+
},
|
|
1451
|
+
[focusBus],
|
|
1452
|
+
);
|
|
1453
|
+
useEffect(() => {
|
|
1454
|
+
const offs = Object.entries(collabById).map(([id, collab]) => {
|
|
1455
|
+
publishFocus(id);
|
|
1456
|
+
return collab.subscribeAwareness(() => publishFocus(id));
|
|
1457
|
+
});
|
|
1458
|
+
return () => {
|
|
1459
|
+
for (const off of offs) off();
|
|
1460
|
+
};
|
|
1461
|
+
}, [collabById, publishFocus]);
|
|
1414
1462
|
// The URL-selected document always materializes as a tab.
|
|
1415
1463
|
useEffect(() => {
|
|
1416
1464
|
// The changes surface has no editor tabs — its selection is a diff, not an
|
|
@@ -2034,6 +2082,7 @@ function CmsView({
|
|
|
2034
2082
|
tabId={t.id}
|
|
2035
2083
|
tab={p}
|
|
2036
2084
|
bus={draftBus}
|
|
2085
|
+
focusBus={focusBus}
|
|
2037
2086
|
initialDocument={anchor ? toPluginDoc(anchor, anchorCollection) : null}
|
|
2038
2087
|
workspaceId={params.workspaceId}
|
|
2039
2088
|
repositoryId={repo.id}
|
|
@@ -2369,6 +2418,16 @@ function CmsView({
|
|
|
2369
2418
|
});
|
|
2370
2419
|
}
|
|
2371
2420
|
}
|
|
2421
|
+
// Which field the author is in, for the preview panes to ring on the
|
|
2422
|
+
// page. Nothing on the changes surface has a field to be in.
|
|
2423
|
+
onFieldFocus={
|
|
2424
|
+
changesMode
|
|
2425
|
+
? undefined
|
|
2426
|
+
: (id, path) => {
|
|
2427
|
+
localFocusRef.current[id] = path;
|
|
2428
|
+
publishFocus(id);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2372
2431
|
renderField={renderField}
|
|
2373
2432
|
media={media}
|
|
2374
2433
|
forms={forms}
|
package/app/src/previewHost.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
usePluginRegistry,
|
|
14
14
|
type PluginDocument,
|
|
15
15
|
type PluginDocumentActionContext,
|
|
16
|
+
type PluginFieldFocus,
|
|
16
17
|
type PluginRegistry,
|
|
17
18
|
type PluginTabSpec,
|
|
18
19
|
} from "@gogitcms/plugin-sdk";
|
|
@@ -93,6 +94,77 @@ export class DraftBus {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
// --- the focus bus ----------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
type FocusListener = (focus: PluginFieldFocus[]) => void;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Who is focused on which field, per document, fanned out to the plugin tabs
|
|
103
|
+
* open on it.
|
|
104
|
+
*
|
|
105
|
+
* Same shape as the draft bus and for the same reason. The value is the
|
|
106
|
+
* whole set for the document — the author at this editor plus every
|
|
107
|
+
* collaborator in its session — so a subscriber paints exactly what it is
|
|
108
|
+
* handed and never has to reconcile deltas. Retained per document so a tab
|
|
109
|
+
* opened while a field is already focused rings it on arrival.
|
|
110
|
+
*/
|
|
111
|
+
export class FocusBus {
|
|
112
|
+
private listeners = new Map<string, Set<FocusListener>>();
|
|
113
|
+
private last = new Map<string, PluginFieldFocus[]>();
|
|
114
|
+
|
|
115
|
+
publish(documentId: string, focus: PluginFieldFocus[]): void {
|
|
116
|
+
const previous = this.last.get(documentId);
|
|
117
|
+
if (previous && sameFocus(previous, focus)) return;
|
|
118
|
+
this.last.set(documentId, focus);
|
|
119
|
+
const set = this.listeners.get(documentId);
|
|
120
|
+
if (!set) return;
|
|
121
|
+
for (const l of set) {
|
|
122
|
+
try {
|
|
123
|
+
l(focus);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(`[plugins] a focus subscriber threw: ${String(err)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
subscribe(documentId: string, listener: FocusListener): () => void {
|
|
131
|
+
let set = this.listeners.get(documentId);
|
|
132
|
+
if (!set) {
|
|
133
|
+
set = new Set();
|
|
134
|
+
this.listeners.set(documentId, set);
|
|
135
|
+
}
|
|
136
|
+
set.add(listener);
|
|
137
|
+
listener(this.last.get(documentId) ?? []);
|
|
138
|
+
return () => {
|
|
139
|
+
set!.delete(listener);
|
|
140
|
+
if (set!.size === 0) this.listeners.delete(documentId);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
forget(documentId: string): void {
|
|
145
|
+
this.last.delete(documentId);
|
|
146
|
+
this.listeners.delete(documentId);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Awareness ticks for reasons other than focus (a cursor, a heartbeat), and
|
|
151
|
+
// each one would otherwise re-post the same set to every preview frame.
|
|
152
|
+
function sameFocus(a: PluginFieldFocus[], b: PluginFieldFocus[]): boolean {
|
|
153
|
+
if (a.length !== b.length) return false;
|
|
154
|
+
return a.every((x, i) => {
|
|
155
|
+
const y = b[i];
|
|
156
|
+
return (
|
|
157
|
+
x.documentId === y.documentId &&
|
|
158
|
+
x.path === y.path &&
|
|
159
|
+
x.field === y.field &&
|
|
160
|
+
x.self === y.self &&
|
|
161
|
+
x.user.id === y.user.id &&
|
|
162
|
+
x.user.name === y.user.name &&
|
|
163
|
+
x.user.color === y.user.color
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
96
168
|
// --- toolbar buttons --------------------------------------------------------
|
|
97
169
|
|
|
98
170
|
export type DocumentActionsProps = {
|
|
@@ -181,6 +253,8 @@ export type PluginTabBodyProps = {
|
|
|
181
253
|
tabId: string;
|
|
182
254
|
tab: PluginTabRef;
|
|
183
255
|
bus: DraftBus;
|
|
256
|
+
/** Who is focused where, for the tab's document. Optional for older hosts. */
|
|
257
|
+
focusBus?: FocusBus;
|
|
184
258
|
/** The document as last loaded, for a tab opened before any edit. */
|
|
185
259
|
initialDocument: PluginDocument | null;
|
|
186
260
|
workspaceId: string;
|
|
@@ -195,13 +269,18 @@ export type PluginTabBodyProps = {
|
|
|
195
269
|
|
|
196
270
|
/** Renders a plugin route inside a column, with its tab context attached. */
|
|
197
271
|
export function PluginTabBody(props: PluginTabBodyProps) {
|
|
198
|
-
const { bus, tab, initialDocument } = props;
|
|
272
|
+
const { bus, focusBus, tab, initialDocument } = props;
|
|
199
273
|
|
|
200
274
|
const onDraft = useCallback(
|
|
201
275
|
(cb: (doc: PluginDocument) => void) =>
|
|
202
276
|
tab.documentId ? bus.subscribe(tab.documentId, cb) : () => {},
|
|
203
277
|
[bus, tab.documentId],
|
|
204
278
|
);
|
|
279
|
+
const onFocus = useCallback(
|
|
280
|
+
(cb: (focus: PluginFieldFocus[]) => void) =>
|
|
281
|
+
focusBus && tab.documentId ? focusBus.subscribe(tab.documentId, cb) : () => {},
|
|
282
|
+
[focusBus, tab.documentId],
|
|
283
|
+
);
|
|
205
284
|
|
|
206
285
|
// The tab context is memoized on its inputs: it is a prop of the plugin's
|
|
207
286
|
// component, and a fresh object each render would re-run every effect the
|
|
@@ -211,10 +290,11 @@ export function PluginTabBody(props: PluginTabBodyProps) {
|
|
|
211
290
|
id: props.tabId,
|
|
212
291
|
document: initialDocument,
|
|
213
292
|
onDraft,
|
|
293
|
+
onFocus,
|
|
214
294
|
openDocument: props.openDocument,
|
|
215
295
|
close: props.close,
|
|
216
296
|
}),
|
|
217
|
-
[props.tabId, initialDocument, onDraft, props.openDocument, props.close],
|
|
297
|
+
[props.tabId, initialDocument, onDraft, onFocus, props.openDocument, props.close],
|
|
218
298
|
);
|
|
219
299
|
|
|
220
300
|
return (
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Which field the author is in, reported as a dotted path — what a preview
|
|
2
|
+
// pane uses to ring the matching part of the page. Read off the DOM's focus
|
|
3
|
+
// events and the `data-cms-field` attribute every control's wrapper carries,
|
|
4
|
+
// so it works for every kind of control and for a lone author with no live
|
|
5
|
+
// session.
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { render, screen, fireEvent, act } from "@testing-library/react";
|
|
8
|
+
import { ThemeProvider } from "../ThemeProvider";
|
|
9
|
+
import { ContentBrowser, type CmsEntry, type CmsNavSection, type EntryField } from "../components/ContentBrowser";
|
|
10
|
+
|
|
11
|
+
jest.mock("../ThemeProvider", () => {
|
|
12
|
+
const actual = jest.requireActual("../ThemeProvider");
|
|
13
|
+
return { ...actual, useResponsive: () => ({ width: 1300, height: 900, isDesktop: true, isMobile: false }) };
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const wrap = (ui: React.ReactElement) => <ThemeProvider>{ui}</ThemeProvider>;
|
|
17
|
+
|
|
18
|
+
const sections: CmsNavSection[] = [
|
|
19
|
+
{ title: "Content", items: [{ key: "pages", label: "Pages", icon: "newspaper" }] },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const fields: EntryField[] = [
|
|
23
|
+
{ name: "title", label: "Title", type: "string", value: "Home", required: true },
|
|
24
|
+
{
|
|
25
|
+
name: "blocks",
|
|
26
|
+
label: "Blocks",
|
|
27
|
+
type: "array",
|
|
28
|
+
component: "mixedList",
|
|
29
|
+
value: [{ _variant: "hero", heading: "Hi", stats: [{ label: "a", value: "1" }] }],
|
|
30
|
+
variants: [
|
|
31
|
+
{
|
|
32
|
+
name: "hero",
|
|
33
|
+
fields: [
|
|
34
|
+
{ name: "heading", label: "Heading", type: "string", value: null },
|
|
35
|
+
{
|
|
36
|
+
name: "stats",
|
|
37
|
+
label: "Stats",
|
|
38
|
+
type: "array",
|
|
39
|
+
of: "object",
|
|
40
|
+
value: null,
|
|
41
|
+
fields: [
|
|
42
|
+
{ name: "label", label: "Label", type: "string", value: null },
|
|
43
|
+
{ name: "value", label: "Value", type: "string", value: null },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const entry: CmsEntry = { id: "doc-1", path: "content/pages/home.json", title: "Home", body: "", fields };
|
|
53
|
+
|
|
54
|
+
function renderBrowser(onFieldFocus = jest.fn()) {
|
|
55
|
+
render(
|
|
56
|
+
wrap(
|
|
57
|
+
<ContentBrowser
|
|
58
|
+
workspace={{ name: "acme/site", initials: "AC", branch: "main", changed: 0 }}
|
|
59
|
+
sections={sections}
|
|
60
|
+
activeNavKey="pages"
|
|
61
|
+
onSelectNav={() => {}}
|
|
62
|
+
entries={[entry]}
|
|
63
|
+
userInitials="ED"
|
|
64
|
+
onSaveEntry={jest.fn()}
|
|
65
|
+
onFieldFocus={onFieldFocus}
|
|
66
|
+
/>,
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
return onFieldFocus;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The blur report is deferred a tick so a move between fields never passes
|
|
73
|
+
// through null; flush it.
|
|
74
|
+
const settle = () => act(() => new Promise((r) => setTimeout(r, 5)));
|
|
75
|
+
|
|
76
|
+
describe("onFieldFocus", () => {
|
|
77
|
+
it("reports a top-level field by name and null when it blurs", async () => {
|
|
78
|
+
const onFieldFocus = renderBrowser();
|
|
79
|
+
const title = screen.getByDisplayValue("Home");
|
|
80
|
+
fireEvent.focusIn(title);
|
|
81
|
+
expect(onFieldFocus).toHaveBeenLastCalledWith("doc-1", "title");
|
|
82
|
+
fireEvent.focusOut(title);
|
|
83
|
+
await settle();
|
|
84
|
+
expect(onFieldFocus).toHaveBeenLastCalledWith("doc-1", null);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("composes the path of a field inside a mixed-list item and a nested list", async () => {
|
|
88
|
+
const onFieldFocus = renderBrowser();
|
|
89
|
+
const heading = screen.getByDisplayValue("Hi");
|
|
90
|
+
fireEvent.focusIn(heading);
|
|
91
|
+
expect(onFieldFocus).toHaveBeenLastCalledWith("doc-1", "blocks.0.heading");
|
|
92
|
+
|
|
93
|
+
const statLabel = screen.getByDisplayValue("a");
|
|
94
|
+
// Straight from one field to the next: no null in between.
|
|
95
|
+
fireEvent.focusOut(heading);
|
|
96
|
+
fireEvent.focusIn(statLabel);
|
|
97
|
+
await settle();
|
|
98
|
+
expect(onFieldFocus).not.toHaveBeenCalledWith("doc-1", null);
|
|
99
|
+
expect(onFieldFocus).toHaveBeenLastCalledWith("doc-1", "blocks.0.stats.0.label");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("annotates every control's wrapper with its path", () => {
|
|
103
|
+
renderBrowser();
|
|
104
|
+
const paths = Array.from(document.querySelectorAll("[data-cms-field]")).map((el) => el.getAttribute("data-cms-field"));
|
|
105
|
+
expect(paths).toEqual(
|
|
106
|
+
expect.arrayContaining(["title", "blocks", "blocks.0", "blocks.0.heading", "blocks.0.stats", "blocks.0.stats.0", "blocks.0.stats.0.label", "blocks.0.stats.0.value"]),
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -336,6 +336,15 @@ export type ContentBrowserProps = {
|
|
|
336
336
|
// already separated from the frontmatter.
|
|
337
337
|
onEntryDraft?: (draft: EntrySaveChange, entry: CmsEntry) => void;
|
|
338
338
|
|
|
339
|
+
// Which field of an open document has keyboard focus, as the author moves
|
|
340
|
+
// between controls: the field's dotted path (`title`, `seo.description`,
|
|
341
|
+
// `blocks.0.heading`), or null when nothing in that document is focused.
|
|
342
|
+
// Web only — it reads the DOM's focus events. This is what lets a preview
|
|
343
|
+
// ring the part of the page the author is editing (§4.4 of the preview
|
|
344
|
+
// design), and it is separate from live collaboration: a lone author gets
|
|
345
|
+
// it too.
|
|
346
|
+
onFieldFocus?: (entryId: string, path: string | null) => void;
|
|
347
|
+
|
|
339
348
|
// Optional per-field renderer. When it returns a node for a field, that node
|
|
340
349
|
// replaces the built-in control (used to inject the markdown body editor).
|
|
341
350
|
renderField?: RenderField;
|
|
@@ -782,8 +791,40 @@ type FieldControlProps = {
|
|
|
782
791
|
// (see FieldSlotArgs.resetNonce); every built-in control is controlled and
|
|
783
792
|
// re-renders from `value` on its own.
|
|
784
793
|
resetNonce?: number;
|
|
794
|
+
// The field's full dotted path, when the caller knows better than
|
|
795
|
+
// "parent path + field name" — a list item, whose control is named after
|
|
796
|
+
// the list but sits at `list.<index>`.
|
|
797
|
+
fieldPath?: string;
|
|
785
798
|
};
|
|
786
799
|
|
|
800
|
+
// The dotted path of the field being rendered, for the controls under it:
|
|
801
|
+
// a group's children append their names to it, a list's items append their
|
|
802
|
+
// index. It exists so every control can carry its own path as a DOM
|
|
803
|
+
// attribute (`data-cms-field`) without any of them threading a prop through
|
|
804
|
+
// — the focus reporting in EntryDetail reads that attribute off whichever
|
|
805
|
+
// element the keyboard lands in.
|
|
806
|
+
//
|
|
807
|
+
// Deliberately separate from the collab `path` prop: that one is threaded
|
|
808
|
+
// only where a CRDT binding exists (top-level fields and drilled-in groups),
|
|
809
|
+
// whereas this is present for every field including list items, which the
|
|
810
|
+
// collab layer treats as one register.
|
|
811
|
+
const FieldPathContext = React.createContext<string>("");
|
|
812
|
+
|
|
813
|
+
function FieldControl(props: FieldControlProps) {
|
|
814
|
+
const prefix = React.useContext(FieldPathContext);
|
|
815
|
+
const fullPath = props.fieldPath ?? (prefix ? `${prefix}.${props.field.name}` : props.field.name);
|
|
816
|
+
return (
|
|
817
|
+
<FieldPathContext.Provider value={fullPath}>
|
|
818
|
+
<View
|
|
819
|
+
// @ts-expect-error react-native-web maps dataSet -> data-* attributes
|
|
820
|
+
dataSet={{ cmsField: fullPath }}
|
|
821
|
+
>
|
|
822
|
+
<FieldControlInner {...props} />
|
|
823
|
+
</View>
|
|
824
|
+
</FieldPathContext.Provider>
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
787
828
|
// The scalar type an array's `of` maps to when rendering item controls.
|
|
788
829
|
function ofToType(of?: string): string {
|
|
789
830
|
switch (of) {
|
|
@@ -794,7 +835,7 @@ function ofToType(of?: string): string {
|
|
|
794
835
|
}
|
|
795
836
|
}
|
|
796
837
|
|
|
797
|
-
function
|
|
838
|
+
function FieldControlInner(props: FieldControlProps) {
|
|
798
839
|
const { field, value, onChange, renderField, readOnly = false, error, hideLabel, onOpenGroup, collab, path, focusSignal, resetNonce } = props;
|
|
799
840
|
const t = useTheme();
|
|
800
841
|
const label = hideLabel ? "" : field.label || field.name;
|
|
@@ -1631,11 +1672,14 @@ function ListControl({
|
|
|
1631
1672
|
...(field.media ? { component: "media", media: field.media, storeAs: field.storeAs } : {}),
|
|
1632
1673
|
};
|
|
1633
1674
|
|
|
1675
|
+
// The list's own path, from the FieldControl wrapping this control; an item
|
|
1676
|
+
// sits at `<list>.<index>`.
|
|
1677
|
+
const listPath = React.useContext(FieldPathContext);
|
|
1634
1678
|
return (
|
|
1635
1679
|
<View style={{ gap: t.space(2) }}>
|
|
1636
1680
|
{items.map((it, i) => (
|
|
1637
1681
|
<ItemFrame key={i} index={i} count={items.length} readOnly={readOnly || !canRemove} onRemove={() => removeItem(i)} onMove={(d) => moveItem(i, d)}>
|
|
1638
|
-
<FieldControl field={itemField} value={it} onChange={(v) => setItem(i, v)} renderField={renderField} readOnly={readOnly} hideLabel />
|
|
1682
|
+
<FieldControl field={itemField} value={it} onChange={(v) => setItem(i, v)} renderField={renderField} readOnly={readOnly} hideLabel fieldPath={listPath ? `${listPath}.${i}` : String(i)} />
|
|
1639
1683
|
</ItemFrame>
|
|
1640
1684
|
))}
|
|
1641
1685
|
{!readOnly && canAdd ? (
|
|
@@ -1675,15 +1719,25 @@ function MixedListControl({
|
|
|
1675
1719
|
onChange(next);
|
|
1676
1720
|
};
|
|
1677
1721
|
const addItem = () => onChange([...items, { [key]: variants[0]?.name ?? "" }]);
|
|
1722
|
+
const listPath = React.useContext(FieldPathContext);
|
|
1678
1723
|
|
|
1679
1724
|
return (
|
|
1680
1725
|
<View style={{ gap: t.space(2) }}>
|
|
1681
1726
|
{items.map((it, i) => {
|
|
1682
1727
|
const variantName = String(it?.[key] ?? "");
|
|
1683
1728
|
const variant = variants.find((v) => v.name === variantName);
|
|
1729
|
+
const itemPath = listPath ? `${listPath}.${i}` : String(i);
|
|
1684
1730
|
return (
|
|
1685
1731
|
<ItemFrame key={i} index={i} count={items.length} readOnly={readOnly || !canRemove} onRemove={() => removeItem(i)} onMove={(d) => moveItem(i, d)}>
|
|
1686
|
-
|
|
1732
|
+
{/* The item's fields compose under `<list>.<index>`; the item
|
|
1733
|
+
itself carries that path so focusing its variant picker
|
|
1734
|
+
reports the item rather than the whole list. */}
|
|
1735
|
+
<FieldPathContext.Provider value={itemPath}>
|
|
1736
|
+
<View
|
|
1737
|
+
style={{ gap: t.space(2) }}
|
|
1738
|
+
// @ts-expect-error react-native-web maps dataSet -> data-* attributes
|
|
1739
|
+
dataSet={{ cmsField: itemPath }}
|
|
1740
|
+
>
|
|
1687
1741
|
<SelectControl
|
|
1688
1742
|
value={variantName}
|
|
1689
1743
|
options={variants.map((v) => v.name)}
|
|
@@ -1701,6 +1755,7 @@ function MixedListControl({
|
|
|
1701
1755
|
/>
|
|
1702
1756
|
) : null}
|
|
1703
1757
|
</View>
|
|
1758
|
+
</FieldPathContext.Provider>
|
|
1704
1759
|
</ItemFrame>
|
|
1705
1760
|
);
|
|
1706
1761
|
})}
|
|
@@ -1745,7 +1800,13 @@ function BodyField({
|
|
|
1745
1800
|
readOnly,
|
|
1746
1801
|
});
|
|
1747
1802
|
return (
|
|
1748
|
-
<View
|
|
1803
|
+
<View
|
|
1804
|
+
style={{ gap: t.space(2), flex: 1 }}
|
|
1805
|
+
// The schema-less body is the document's one field; a preview rings
|
|
1806
|
+
// it under the same name the schema form would give it.
|
|
1807
|
+
// @ts-expect-error react-native-web maps dataSet -> data-* attributes
|
|
1808
|
+
dataSet={{ cmsField: "body" }}
|
|
1809
|
+
>
|
|
1749
1810
|
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
|
1750
1811
|
<Text variant="label" color="tertiary">Body</Text>
|
|
1751
1812
|
<Text variant="monoSm" color="tertiary">Markdown</Text>
|
|
@@ -2597,6 +2658,7 @@ function EntryDetail({
|
|
|
2597
2658
|
entry,
|
|
2598
2659
|
documentActions,
|
|
2599
2660
|
onEntryDraft,
|
|
2661
|
+
onFieldFocus,
|
|
2600
2662
|
renderField,
|
|
2601
2663
|
onSaveEntry,
|
|
2602
2664
|
readOnly = false,
|
|
@@ -2619,6 +2681,8 @@ function EntryDetail({
|
|
|
2619
2681
|
documentActions?: (entry: CmsEntry) => React.ReactNode;
|
|
2620
2682
|
// Debounced in-flight draft (see ContentBrowserProps.onEntryDraft).
|
|
2621
2683
|
onEntryDraft?: (draft: EntrySaveChange, entry: CmsEntry) => void;
|
|
2684
|
+
// The focused field's path, or null (see ContentBrowserProps.onFieldFocus).
|
|
2685
|
+
onFieldFocus?: (entryId: string, path: string | null) => void;
|
|
2622
2686
|
renderField?: RenderField;
|
|
2623
2687
|
onSaveEntry?: SaveEntry;
|
|
2624
2688
|
readOnly?: boolean;
|
|
@@ -2972,8 +3036,54 @@ function EntryDetail({
|
|
|
2972
3036
|
// The field list + nested value object for the active frame.
|
|
2973
3037
|
const frame = resolveFrame(editFields, values, groupPath);
|
|
2974
3038
|
|
|
3039
|
+
// Which field has the keyboard. One pair of DOM focus listeners on the
|
|
3040
|
+
// column rather than an onFocus on every control: the controls are many
|
|
3041
|
+
// (inputs, selects, the ProseMirror body, plugin fields) and every one of
|
|
3042
|
+
// them renders inside the FieldControl wrapper that carries the path as
|
|
3043
|
+
// `data-cms-field` — so the element the focus landed in is enough.
|
|
3044
|
+
//
|
|
3045
|
+
// focusout fires before the next focusin, so a blur is reported a tick
|
|
3046
|
+
// late and cancelled if focus went straight to another field: moving
|
|
3047
|
+
// between two controls reads as one change, not a flicker through null.
|
|
3048
|
+
const focusRoot = useRef<View>(null);
|
|
3049
|
+
const fieldFocus = useRef(onFieldFocus);
|
|
3050
|
+
fieldFocus.current = onFieldFocus;
|
|
3051
|
+
const entryId = entry.id;
|
|
3052
|
+
useEffect(() => {
|
|
3053
|
+
if (Platform.OS !== "web") return;
|
|
3054
|
+
const node = focusRoot.current as unknown as HTMLElement | null;
|
|
3055
|
+
if (!node || typeof node.addEventListener !== "function") return;
|
|
3056
|
+
let pending: ReturnType<typeof setTimeout> | null = null;
|
|
3057
|
+
let last: string | null = null;
|
|
3058
|
+
const report = (path: string | null) => {
|
|
3059
|
+
if (path === last) return;
|
|
3060
|
+
last = path;
|
|
3061
|
+
fieldFocus.current?.(entryId, path);
|
|
3062
|
+
};
|
|
3063
|
+
const onFocusIn = (ev: Event) => {
|
|
3064
|
+
if (pending) { clearTimeout(pending); pending = null; }
|
|
3065
|
+
const target = ev.target as Element | null;
|
|
3066
|
+
const el = target && typeof target.closest === "function" ? target.closest("[data-cms-field]") : null;
|
|
3067
|
+
report(el?.getAttribute("data-cms-field") || null);
|
|
3068
|
+
};
|
|
3069
|
+
const onFocusOut = () => {
|
|
3070
|
+
if (pending) clearTimeout(pending);
|
|
3071
|
+
pending = setTimeout(() => { pending = null; report(null); }, 0);
|
|
3072
|
+
};
|
|
3073
|
+
node.addEventListener("focusin", onFocusIn);
|
|
3074
|
+
node.addEventListener("focusout", onFocusOut);
|
|
3075
|
+
return () => {
|
|
3076
|
+
node.removeEventListener("focusin", onFocusIn);
|
|
3077
|
+
node.removeEventListener("focusout", onFocusOut);
|
|
3078
|
+
if (pending) clearTimeout(pending);
|
|
3079
|
+
// The column is going away with the field still focused: nothing in
|
|
3080
|
+
// this document has focus any more.
|
|
3081
|
+
if (last !== null) fieldFocus.current?.(entryId, null);
|
|
3082
|
+
};
|
|
3083
|
+
}, [entryId]);
|
|
3084
|
+
|
|
2975
3085
|
return (
|
|
2976
|
-
<View style={{ flex: 1 }}>
|
|
3086
|
+
<View style={{ flex: 1 }} ref={focusRoot}>
|
|
2977
3087
|
{/* breadcrumb + actions. position/zIndex lift this row (and the "..." menu
|
|
2978
3088
|
dropdown it hosts) above the content pane so the menu receives clicks. */}
|
|
2979
3089
|
<View
|
|
@@ -3129,6 +3239,8 @@ function EntryDetail({
|
|
|
3129
3239
|
<Text variant="monoSm" color="tertiary">{frame.labels.join(" / ")}</Text>
|
|
3130
3240
|
</Pressable>
|
|
3131
3241
|
) : null}
|
|
3242
|
+
{/* Drilled into a group, every field's path starts with the group's. */}
|
|
3243
|
+
<FieldPathContext.Provider value={groupPath.join(".")}>
|
|
3132
3244
|
{frame.fields.map((f) => (
|
|
3133
3245
|
<FieldControl
|
|
3134
3246
|
key={f.name}
|
|
@@ -3147,6 +3259,7 @@ function EntryDetail({
|
|
|
3147
3259
|
resetNonce={resetNonces[f.name]}
|
|
3148
3260
|
/>
|
|
3149
3261
|
))}
|
|
3262
|
+
</FieldPathContext.Provider>
|
|
3150
3263
|
</ScrollView>
|
|
3151
3264
|
</DocumentPathProvider>
|
|
3152
3265
|
) : (
|
|
@@ -4133,6 +4246,7 @@ function DesktopBrowser(props: ContentBrowserProps) {
|
|
|
4133
4246
|
documentActions={props.documentActions}
|
|
4134
4247
|
history={props.history}
|
|
4135
4248
|
onEntryDraft={props.onEntryDraft}
|
|
4249
|
+
onFieldFocus={props.onFieldFocus}
|
|
4136
4250
|
renderField={renderField}
|
|
4137
4251
|
onSaveEntry={props.onSaveEntry}
|
|
4138
4252
|
readOnly={col.readOnly}
|
|
@@ -5009,6 +5123,7 @@ function MobileBrowser(props: ContentBrowserProps) {
|
|
|
5009
5123
|
documentActions={props.documentActions}
|
|
5010
5124
|
history={props.history}
|
|
5011
5125
|
onEntryDraft={props.onEntryDraft}
|
|
5126
|
+
onFieldFocus={props.onFieldFocus}
|
|
5012
5127
|
renderField={renderField}
|
|
5013
5128
|
onSaveEntry={props.onSaveEntry}
|
|
5014
5129
|
readOnly={readOnly}
|
|
@@ -32,6 +32,24 @@ export type PluginDocument = {
|
|
|
32
32
|
bodyField?: string;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* One field somebody is focused on in the editor, and who.
|
|
37
|
+
*
|
|
38
|
+
* Structurally the preview protocol's `PreviewFocus`, so a preview plugin can
|
|
39
|
+
* hand the list straight to the page. `self` marks the user driving this
|
|
40
|
+
* editor; the rest are collaborators from the document's live session, with
|
|
41
|
+
* the presence colour the editor already shows beside their field.
|
|
42
|
+
*/
|
|
43
|
+
export type PluginFieldFocus = {
|
|
44
|
+
documentId: string;
|
|
45
|
+
/** Repo-relative path of the document. */
|
|
46
|
+
path: string;
|
|
47
|
+
/** Dotted field path: `title`, `seo.description`, `blocks.0.heading`. */
|
|
48
|
+
field: string;
|
|
49
|
+
user: { id: string; name: string; color: string };
|
|
50
|
+
self: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
35
53
|
/**
|
|
36
54
|
* Extra props a route gets when it is rendered as a *tab* beside the editor
|
|
37
55
|
* rather than as a full screen at its URL.
|
|
@@ -51,6 +69,14 @@ export type PluginTabContext = {
|
|
|
51
69
|
* unsubscribe function.
|
|
52
70
|
*/
|
|
53
71
|
onDraft: (cb: (doc: PluginDocument) => void) => () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Subscribe to who is focused on which of that document's fields — the
|
|
74
|
+
* user at this editor and every collaborator in its live session. Called
|
|
75
|
+
* with the full set on every change, and once on subscribe with the current
|
|
76
|
+
* one. Optional: an older host has no notion of it, and a plugin treats its
|
|
77
|
+
* absence as "nobody is focused".
|
|
78
|
+
*/
|
|
79
|
+
onFocus?: (cb: (focus: PluginFieldFocus[]) => void) => () => void;
|
|
54
80
|
/** Open a document in the editor — the inverse direction, for click-to-edit. */
|
|
55
81
|
openDocument: (id: string, field?: string) => void;
|
|
56
82
|
/** Close this tab. */
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gogitcms/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@gogitcms/editor",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.42.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@apollo/client": "^3.11.8",
|
|
12
12
|
"@handlewithcare/react-prosemirror": "^3.2.7",
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"node": ">=20"
|
|
48
48
|
},
|
|
49
49
|
"optionalDependencies": {
|
|
50
|
-
"@gogitcms/gitcms-local-darwin-arm64": "0.
|
|
51
|
-
"@gogitcms/gitcms-local-darwin-x64": "0.
|
|
52
|
-
"@gogitcms/gitcms-local-linux-arm64": "0.
|
|
53
|
-
"@gogitcms/gitcms-local-linux-x64": "0.
|
|
54
|
-
"@gogitcms/gitcms-local-win32-x64": "0.
|
|
50
|
+
"@gogitcms/gitcms-local-darwin-arm64": "0.42.0",
|
|
51
|
+
"@gogitcms/gitcms-local-darwin-x64": "0.42.0",
|
|
52
|
+
"@gogitcms/gitcms-local-linux-arm64": "0.42.0",
|
|
53
|
+
"@gogitcms/gitcms-local-linux-x64": "0.42.0",
|
|
54
|
+
"@gogitcms/gitcms-local-win32-x64": "0.42.0"
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"node_modules/@apollo/client": {
|
|
@@ -751,9 +751,9 @@
|
|
|
751
751
|
}
|
|
752
752
|
},
|
|
753
753
|
"node_modules/@gogitcms/gitcms-local-darwin-arm64": {
|
|
754
|
-
"version": "0.
|
|
755
|
-
"resolved": "https://registry.npmjs.org/@gogitcms/gitcms-local-darwin-arm64/-/gitcms-local-darwin-arm64-0.
|
|
756
|
-
"integrity": "sha512-
|
|
754
|
+
"version": "0.42.0",
|
|
755
|
+
"resolved": "https://registry.npmjs.org/@gogitcms/gitcms-local-darwin-arm64/-/gitcms-local-darwin-arm64-0.42.0.tgz",
|
|
756
|
+
"integrity": "sha512-rPnqWdexcwEU+jlQc8gRjbYI89uXjBpJYewD5wZz41bRFDVwW93klZw8tJHu108GHihzFh5WnVz0ga1AMWisIw==",
|
|
757
757
|
"cpu": [
|
|
758
758
|
"arm64"
|
|
759
759
|
],
|
|
@@ -764,9 +764,9 @@
|
|
|
764
764
|
]
|
|
765
765
|
},
|
|
766
766
|
"node_modules/@gogitcms/gitcms-local-darwin-x64": {
|
|
767
|
-
"version": "0.
|
|
768
|
-
"resolved": "https://registry.npmjs.org/@gogitcms/gitcms-local-darwin-x64/-/gitcms-local-darwin-x64-0.
|
|
769
|
-
"integrity": "sha512-
|
|
767
|
+
"version": "0.42.0",
|
|
768
|
+
"resolved": "https://registry.npmjs.org/@gogitcms/gitcms-local-darwin-x64/-/gitcms-local-darwin-x64-0.42.0.tgz",
|
|
769
|
+
"integrity": "sha512-cqziOuLSOFJOTGzXmVSrkXROtcvJm44Dro+fjB4iluTdRdtvmt/g4KmJEuATKYVpwBn7nJqKnv+eIwezwkaAFQ==",
|
|
770
770
|
"cpu": [
|
|
771
771
|
"x64"
|
|
772
772
|
],
|
|
@@ -777,9 +777,9 @@
|
|
|
777
777
|
]
|
|
778
778
|
},
|
|
779
779
|
"node_modules/@gogitcms/gitcms-local-linux-x64": {
|
|
780
|
-
"version": "0.
|
|
781
|
-
"resolved": "https://registry.npmjs.org/@gogitcms/gitcms-local-linux-x64/-/gitcms-local-linux-x64-0.
|
|
782
|
-
"integrity": "sha512-
|
|
780
|
+
"version": "0.42.0",
|
|
781
|
+
"resolved": "https://registry.npmjs.org/@gogitcms/gitcms-local-linux-x64/-/gitcms-local-linux-x64-0.42.0.tgz",
|
|
782
|
+
"integrity": "sha512-hjcY5voX1Do9+BDg9QAAC2SEVLswsm/rHt38teLHKoXCEcFa6E90O2sVj9DqTsxS3HKYJoI8dy92iGoU3kkNYA==",
|
|
783
783
|
"cpu": [
|
|
784
784
|
"x64"
|
|
785
785
|
],
|
|
@@ -790,9 +790,9 @@
|
|
|
790
790
|
]
|
|
791
791
|
},
|
|
792
792
|
"node_modules/@gogitcms/gitcms-local-win32-x64": {
|
|
793
|
-
"version": "0.
|
|
794
|
-
"resolved": "https://registry.npmjs.org/@gogitcms/gitcms-local-win32-x64/-/gitcms-local-win32-x64-0.
|
|
795
|
-
"integrity": "sha512-
|
|
793
|
+
"version": "0.42.0",
|
|
794
|
+
"resolved": "https://registry.npmjs.org/@gogitcms/gitcms-local-win32-x64/-/gitcms-local-win32-x64-0.42.0.tgz",
|
|
795
|
+
"integrity": "sha512-0Po8OeY9Q43zhvEc6JnzQTInlZ4VVcgli6MDOYxrenTSyTDQmKVySrKeZkqOnDZfZzibqkcLe/JNlR7WOgOHvw==",
|
|
796
796
|
"cpu": [
|
|
797
797
|
"x64"
|
|
798
798
|
],
|
|
@@ -969,18 +969,18 @@
|
|
|
969
969
|
}
|
|
970
970
|
},
|
|
971
971
|
"node_modules/@posthog/core": {
|
|
972
|
-
"version": "1.50.
|
|
973
|
-
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.50.
|
|
974
|
-
"integrity": "sha512-
|
|
972
|
+
"version": "1.50.5",
|
|
973
|
+
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.50.5.tgz",
|
|
974
|
+
"integrity": "sha512-afEchuShDaVIoxAIj76kDZQ1DhfesDmgfVp+mtTzsA3wlc8DF5uoz8YjuTjnxOicWkpP5HCDqYidK/1kT125Cg==",
|
|
975
975
|
"license": "MIT",
|
|
976
976
|
"dependencies": {
|
|
977
|
-
"@posthog/types": "^1.
|
|
977
|
+
"@posthog/types": "^1.409.0"
|
|
978
978
|
}
|
|
979
979
|
},
|
|
980
980
|
"node_modules/@posthog/types": {
|
|
981
|
-
"version": "1.
|
|
982
|
-
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.
|
|
983
|
-
"integrity": "sha512-
|
|
981
|
+
"version": "1.409.0",
|
|
982
|
+
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.409.0.tgz",
|
|
983
|
+
"integrity": "sha512-239umoaZVb2GBaXeEyJpwFvjhrrChJH8NHCwiao23EBSu3NA6EN0MTMoaHSmMEf4yjiXEFFoYJA6FjJAXF5HGA==",
|
|
984
984
|
"license": "MIT"
|
|
985
985
|
},
|
|
986
986
|
"node_modules/@react-native/asset-utils": {
|
|
@@ -2315,9 +2315,9 @@
|
|
|
2315
2315
|
"peer": true
|
|
2316
2316
|
},
|
|
2317
2317
|
"node_modules/electron-to-chromium": {
|
|
2318
|
-
"version": "1.5.
|
|
2319
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
|
2320
|
-
"integrity": "sha512-
|
|
2318
|
+
"version": "1.5.422",
|
|
2319
|
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz",
|
|
2320
|
+
"integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==",
|
|
2321
2321
|
"license": "ISC"
|
|
2322
2322
|
},
|
|
2323
2323
|
"node_modules/emoji-regex": {
|
|
@@ -4058,14 +4058,14 @@
|
|
|
4058
4058
|
"license": "MIT"
|
|
4059
4059
|
},
|
|
4060
4060
|
"node_modules/posthog-js": {
|
|
4061
|
-
"version": "1.
|
|
4062
|
-
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.
|
|
4063
|
-
"integrity": "sha512-
|
|
4061
|
+
"version": "1.427.1",
|
|
4062
|
+
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.427.1.tgz",
|
|
4063
|
+
"integrity": "sha512-jPKpF5qQS+SIzg2kJH6mTqkMEVY9YVBEywIccLFdl8A7zn+9aSnXQVv4xbF5gIjPtQe2MdcDugcyyJqfVrImLw==",
|
|
4064
4064
|
"license": "(Apache-2.0 AND MIT)",
|
|
4065
4065
|
"dependencies": {
|
|
4066
4066
|
"@posthog/browser-common": "^0.7.2",
|
|
4067
|
-
"@posthog/core": "^1.50.
|
|
4068
|
-
"@posthog/types": "^1.
|
|
4067
|
+
"@posthog/core": "^1.50.5",
|
|
4068
|
+
"@posthog/types": "^1.409.0",
|
|
4069
4069
|
"core-js": "^3.49.0",
|
|
4070
4070
|
"dompurify": "^3.4.13",
|
|
4071
4071
|
"fflate": "^0.4.8",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gogitcms/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Terminal UI for setting up and running the Go·Git CMS content editor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -65,10 +65,10 @@
|
|
|
65
65
|
"@gogitcms/frontend": "workspace:*"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@gogitcms/gitcms-local-darwin-arm64": "0.
|
|
69
|
-
"@gogitcms/gitcms-local-darwin-x64": "0.
|
|
70
|
-
"@gogitcms/gitcms-local-linux-arm64": "0.
|
|
71
|
-
"@gogitcms/gitcms-local-linux-x64": "0.
|
|
72
|
-
"@gogitcms/gitcms-local-win32-x64": "0.
|
|
68
|
+
"@gogitcms/gitcms-local-darwin-arm64": "0.42.0",
|
|
69
|
+
"@gogitcms/gitcms-local-darwin-x64": "0.42.0",
|
|
70
|
+
"@gogitcms/gitcms-local-linux-arm64": "0.42.0",
|
|
71
|
+
"@gogitcms/gitcms-local-linux-x64": "0.42.0",
|
|
72
|
+
"@gogitcms/gitcms-local-win32-x64": "0.42.0"
|
|
73
73
|
}
|
|
74
74
|
}
|