@gogitcms/design-system 0.16.0-next.11 → 0.16.0-next.13
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/package.json +1 -1
- package/src/__tests__/ContentBrowser.media.test.tsx +2 -1
- package/src/__tests__/ContentBrowser.references.test.tsx +84 -3
- package/src/components/ContentBrowser.tsx +66 -21
- package/src/components/DocumentDetails.tsx +0 -0
- package/src/components/MediaField.tsx +2 -2
- package/src/components/Notifications.tsx +10 -1
- package/src/components/ReferenceField.tsx +0 -0
- package/src/index.ts +3 -2
- package/src/references.ts +11 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gogitcms/design-system",
|
|
3
|
-
"version": "0.16.0-next.
|
|
3
|
+
"version": "0.16.0-next.13",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"types": "src/index.ts",
|
|
6
6
|
"// exports": "The root entry is the react-native source the SPAs, desktop app and mobile app consume. ./web is the plain-DOM build for server-rendered surfaces (the Astro docs site) that don't run react-native-web, and ./css ships the tokens as custom properties. The trailing wildcard keeps deep paths resolvable — plugin bundling and the Vite aliases reach into src/ directly.",
|
|
@@ -68,6 +68,7 @@ const galleryField: EntryField = {
|
|
|
68
68
|
type: "array",
|
|
69
69
|
of: "string",
|
|
70
70
|
component: "list",
|
|
71
|
+
itemComponent: "media",
|
|
71
72
|
media: "uploads",
|
|
72
73
|
storeAs: "public",
|
|
73
74
|
value: ["/uploads/hero.png"],
|
|
@@ -89,7 +90,7 @@ test("array items render media pickers when the array names a media set", async
|
|
|
89
90
|
});
|
|
90
91
|
|
|
91
92
|
test("a list with no media set stays a plain list of inputs", () => {
|
|
92
|
-
renderWith([{ ...galleryField, media: undefined, storeAs: undefined, value: ["x"] }]);
|
|
93
|
+
renderWith([{ ...galleryField, itemComponent: undefined, media: undefined, storeAs: undefined, value: ["x"] }]);
|
|
93
94
|
expect(screen.queryByTestId("media-choose")).not.toBeInTheDocument();
|
|
94
95
|
});
|
|
95
96
|
|
|
@@ -44,8 +44,13 @@ const embedDef: ReferenceDef = {
|
|
|
44
44
|
|
|
45
45
|
// `null` renders without a references seam; the default is a working one.
|
|
46
46
|
// (A default parameter also fires for an explicit `undefined`, so the no-seam
|
|
47
|
-
// case has to be spelled `null`.)
|
|
48
|
-
|
|
47
|
+
// case has to be spelled `null`.) `extra` is spread over the browser's props —
|
|
48
|
+
// the host's open-document callback, for the tests that need one.
|
|
49
|
+
function renderWith(
|
|
50
|
+
fields: EntryField[],
|
|
51
|
+
api: ReferenceApi | null = makeApi(),
|
|
52
|
+
extra: Partial<React.ComponentProps<typeof ContentBrowser>> = {},
|
|
53
|
+
) {
|
|
49
54
|
const onSave = jest.fn();
|
|
50
55
|
const entry: CmsEntry = { id: "a", path: "content/articles/a.md", title: "Alpha", body: "", fields };
|
|
51
56
|
render(
|
|
@@ -59,6 +64,7 @@ function renderWith(fields: EntryField[], api: ReferenceApi | null = makeApi())
|
|
|
59
64
|
userInitials="ED"
|
|
60
65
|
references={api ?? undefined}
|
|
61
66
|
onSaveEntry={onSave}
|
|
67
|
+
{...extra}
|
|
62
68
|
/>
|
|
63
69
|
</ThemeProvider>,
|
|
64
70
|
);
|
|
@@ -123,12 +129,87 @@ test("an object reference shows its embedded copies read-only and picks by key c
|
|
|
123
129
|
|
|
124
130
|
test("an array of references renders a picker per item", async () => {
|
|
125
131
|
renderWith([
|
|
126
|
-
{ name: "related", label: "Related", type: "array", of: "string", component: "list", reference: authorDef, value: [ada.key, bob.key] },
|
|
132
|
+
{ name: "related", label: "Related", type: "array", of: "string", component: "list", itemComponent: "reference", reference: authorDef, value: [ada.key, bob.key] },
|
|
127
133
|
]);
|
|
128
134
|
expect(await screen.findAllByTestId("reference-choose")).toHaveLength(2);
|
|
129
135
|
expect(await screen.findByText("Bob")).toBeInTheDocument();
|
|
130
136
|
});
|
|
131
137
|
|
|
138
|
+
test("Add item on a reference list opens the picker and appends the pick", async () => {
|
|
139
|
+
renderWith([
|
|
140
|
+
{ name: "related", label: "Related", type: "array", of: "string", component: "list", itemComponent: "reference", reference: authorDef, value: [ada.key] },
|
|
141
|
+
]);
|
|
142
|
+
expect(await screen.findByText("Ada Lovelace")).toBeInTheDocument();
|
|
143
|
+
|
|
144
|
+
fireEvent.click(screen.getByTestId("list-add"));
|
|
145
|
+
// No empty slot appeared — the picker did — and the pick becomes the item.
|
|
146
|
+
expect(screen.getAllByTestId("reference-choose")).toHaveLength(1);
|
|
147
|
+
fireEvent.click(await screen.findByTestId(`reference-row-${bob.id}`));
|
|
148
|
+
|
|
149
|
+
expect(await screen.findByText("Bob")).toBeInTheDocument();
|
|
150
|
+
expect(screen.getAllByTestId("reference-choose")).toHaveLength(2);
|
|
151
|
+
expect(screen.getAllByTestId("reference-key").map((el) => el.textContent)).toEqual([ada.key, bob.key]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("Add item on a reference list without a seam is disabled rather than adding a slot", async () => {
|
|
155
|
+
renderWith(
|
|
156
|
+
[{ name: "related", label: "Related", type: "array", of: "string", component: "list", itemComponent: "reference", reference: authorDef, value: [] }],
|
|
157
|
+
null,
|
|
158
|
+
);
|
|
159
|
+
fireEvent.click(await screen.findByTestId("list-add"));
|
|
160
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
161
|
+
expect(screen.queryByTestId("reference-choose")).not.toBeInTheDocument();
|
|
162
|
+
expect(screen.queryByText("Choose a document")).not.toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a resolved target can be opened in a new pane, handing the document to the host", async () => {
|
|
166
|
+
const onOpenReference = jest.fn();
|
|
167
|
+
renderWith(
|
|
168
|
+
[{ name: "author", label: "Author", type: "string", component: "reference", reference: authorDef, value: ada.key }],
|
|
169
|
+
makeApi(),
|
|
170
|
+
{ onOpenReference },
|
|
171
|
+
);
|
|
172
|
+
expect(await screen.findByText("Ada Lovelace")).toBeInTheDocument();
|
|
173
|
+
fireEvent.click(screen.getByTestId("reference-open"));
|
|
174
|
+
expect(onOpenReference).toHaveBeenCalledWith(
|
|
175
|
+
expect.objectContaining({ id: ada.id, collection: "authors", path: ada.path, label: "Ada Lovelace" }),
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("without a host that opens documents the Open action is not offered", async () => {
|
|
180
|
+
renderWith([{ name: "author", label: "Author", type: "string", component: "reference", reference: authorDef, value: ada.key }]);
|
|
181
|
+
expect(await screen.findByText("Ada Lovelace")).toBeInTheDocument();
|
|
182
|
+
expect(screen.queryByTestId("reference-open")).not.toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("the Details modal opens a reference or a referrer in a new pane and closes itself", async () => {
|
|
186
|
+
const onOpenReference = jest.fn();
|
|
187
|
+
renderWith(
|
|
188
|
+
[{ name: "author", label: "Author", type: "string", component: "reference", reference: authorDef, value: ada.key }],
|
|
189
|
+
makeApi(),
|
|
190
|
+
{ onOpenReference },
|
|
191
|
+
);
|
|
192
|
+
await screen.findByText("Ada Lovelace");
|
|
193
|
+
|
|
194
|
+
fireEvent.click(screen.getByTestId("detail-menu"));
|
|
195
|
+
fireEvent.click(screen.getByTestId("detail-details"));
|
|
196
|
+
const refs = await screen.findByTestId("details-references");
|
|
197
|
+
await waitFor(() => expect(refs).toHaveTextContent("Ada Lovelace"));
|
|
198
|
+
fireEvent.click(screen.getByTestId(`details-open-${ada.id}`));
|
|
199
|
+
expect(onOpenReference).toHaveBeenLastCalledWith(expect.objectContaining({ id: ada.id, collection: "authors" }));
|
|
200
|
+
expect(screen.queryByTestId("document-details")).not.toBeInTheDocument();
|
|
201
|
+
|
|
202
|
+
fireEvent.click(screen.getByTestId("detail-menu"));
|
|
203
|
+
fireEvent.click(screen.getByTestId("detail-details"));
|
|
204
|
+
const referrers = await screen.findByTestId("details-referrers");
|
|
205
|
+
await waitFor(() => expect(referrers).toHaveTextContent("Other"));
|
|
206
|
+
fireEvent.click(screen.getByTestId("details-open-d2"));
|
|
207
|
+
expect(onOpenReference).toHaveBeenLastCalledWith(
|
|
208
|
+
expect.objectContaining({ id: "d2", collection: "articles", path: "content/articles/other.md", label: "Other" }),
|
|
209
|
+
);
|
|
210
|
+
expect(screen.queryByTestId("document-details")).not.toBeInTheDocument();
|
|
211
|
+
});
|
|
212
|
+
|
|
132
213
|
test("the Details modal shows the document's metadata and its references both ways", async () => {
|
|
133
214
|
renderWith([
|
|
134
215
|
{ name: "title", label: "Title", type: "string", value: "Alpha" },
|
|
@@ -32,9 +32,10 @@ import {
|
|
|
32
32
|
} from "./CollabField";
|
|
33
33
|
import { clamp, reorder, slotAtX } from "./reorder";
|
|
34
34
|
import { MediaField, MediaProvider, DocumentPathProvider, useFieldMedia } from "./MediaField";
|
|
35
|
-
import { ReferenceField, ReferenceProvider } from "./ReferenceField";
|
|
35
|
+
import { ReferenceField, ReferencePicker, ReferenceProvider, useReferenceApi } from "./ReferenceField";
|
|
36
36
|
import { DocumentDetails } from "./DocumentDetails";
|
|
37
|
-
import type { ReferenceApi, ReferenceDef } from "../references";
|
|
37
|
+
import type { ReferenceApi, ReferenceDef, ReferenceDocument, ReferenceTarget } from "../references";
|
|
38
|
+
import { referenceValue } from "../references";
|
|
38
39
|
import { MediaBrowser } from "./MediaBrowser";
|
|
39
40
|
import { FormsBrowser } from "./FormsBrowser";
|
|
40
41
|
import { FORMS_NAV_KEY, formsNavKey, isFormsNavKey, parseFormsNavKey, type FormsApi } from "../forms";
|
|
@@ -73,21 +74,26 @@ export type EntryField = {
|
|
|
73
74
|
source?: string;
|
|
74
75
|
format?: string;
|
|
75
76
|
flavor?: string;
|
|
76
|
-
// For a media picker (`component: "media"
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// field
|
|
77
|
+
// For a media picker (`component: "media"`, or an array whose items are —
|
|
78
|
+
// `itemComponent: "media"`): which configured media set to browse, and —
|
|
79
|
+
// for a string value — which form of the path to write. Both come from the
|
|
80
|
+
// field's `media:` block; on an array they describe its *items*.
|
|
80
81
|
media?: string;
|
|
81
82
|
storeAs?: string;
|
|
82
83
|
// For a document picker (`component: "reference"`, or an array whose items
|
|
83
|
-
// are
|
|
84
|
-
// reference
|
|
85
|
-
// embedded copies —
|
|
86
|
-
//
|
|
84
|
+
// are — `itemComponent: "reference"`): what the field points at and stores.
|
|
85
|
+
// An object-shaped reference declares its `fields` — the key child and the
|
|
86
|
+
// embedded copies — and the copies are the server's: recomputed on every
|
|
87
|
+
// save and shown read-only here (docs/references.md).
|
|
87
88
|
reference?: ReferenceDef;
|
|
88
89
|
value: unknown;
|
|
89
90
|
// Item type for an array field (string | number | boolean | object).
|
|
90
91
|
of?: string;
|
|
92
|
+
// The control each item of an array renders, from display.item.component.
|
|
93
|
+
// `list` describes the array; what an item is — a text input, a media
|
|
94
|
+
// picker, a document picker — is declared here, never inferred from the
|
|
95
|
+
// array's media or reference block. Absent means the default for `of`.
|
|
96
|
+
itemComponent?: string;
|
|
91
97
|
// Child fields for an object field (or an array whose `of` is object).
|
|
92
98
|
fields?: EntryField[];
|
|
93
99
|
// Object shapes a mixed-list array (component: "mixedList") can hold.
|
|
@@ -373,6 +379,12 @@ export type ContentBrowserProps = {
|
|
|
373
379
|
// the document picker and the "Referenced by" list, the host owns fetching.
|
|
374
380
|
// Omitted → reference fields render read-only.
|
|
375
381
|
references?: ReferenceApi;
|
|
382
|
+
// Open a referenced document — the Open action on a reference field's target
|
|
383
|
+
// and on the rows of the Details modal. The document may belong to any
|
|
384
|
+
// collection, so the host, which owns the open columns and the URL, does the
|
|
385
|
+
// opening (a new column on desktop, in place on mobile). Omitted, the action
|
|
386
|
+
// is not offered.
|
|
387
|
+
onOpenReference?: (doc: ReferenceDocument) => void;
|
|
376
388
|
|
|
377
389
|
// Forms data seam, the same split (docs/forms.md §10.2). Omitted → the Forms
|
|
378
390
|
// surface never renders, which is what a config declaring no forms looks like.
|
|
@@ -1704,22 +1716,44 @@ function ListControl({
|
|
|
1704
1716
|
[next[i], next[j]] = [next[j], next[i]];
|
|
1705
1717
|
onChange(next);
|
|
1706
1718
|
};
|
|
1707
|
-
|
|
1719
|
+
// A reference list's "Add item" opens the picker straight away and appends
|
|
1720
|
+
// what was picked: the only thing such an item can hold is a picked
|
|
1721
|
+
// document, so an empty slot to fill in afterwards is a step nobody wants.
|
|
1722
|
+
// Without a host seam there is nothing to pick from, and the button is
|
|
1723
|
+
// disabled as the field's own Choose button is. "Reference list" is the
|
|
1724
|
+
// item's declared control, not the presence of a block: an array whose
|
|
1725
|
+
// items are not declared pickers is a plain list whatever else it carries.
|
|
1726
|
+
const reference = field.itemComponent === "reference" ? field.reference : undefined;
|
|
1727
|
+
const referenceApi = useReferenceApi();
|
|
1728
|
+
const [pickingNew, setPickingNew] = React.useState(false);
|
|
1729
|
+
const addItem = () => {
|
|
1730
|
+
if (reference) {
|
|
1731
|
+
if (referenceApi) setPickingNew(true);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
onChange([...items, isObject ? {} : ""]);
|
|
1735
|
+
};
|
|
1736
|
+
const addPicked = (target: ReferenceTarget) => {
|
|
1737
|
+
setPickingNew(false);
|
|
1738
|
+
if (!reference) return;
|
|
1739
|
+
onChange([...items, referenceValue(target, isObject ? "object" : "string", reference, undefined)]);
|
|
1740
|
+
};
|
|
1708
1741
|
|
|
1709
|
-
// Items
|
|
1710
|
-
// component — `list` describes the array, and is not a
|
|
1711
|
-
//
|
|
1712
|
-
//
|
|
1742
|
+
// Items render the array's *declared* item control (display.item.component),
|
|
1743
|
+
// not the array's own component — `list` describes the array, and is not a
|
|
1744
|
+
// control an item could render. The media and reference blocks describe the
|
|
1745
|
+
// items too, so they travel with the item; they never pick its control. An
|
|
1746
|
+
// array that declares nothing falls back to the default control for `of`.
|
|
1713
1747
|
const itemField: EntryField = {
|
|
1714
1748
|
name: field.name,
|
|
1715
1749
|
type: itemType,
|
|
1716
1750
|
of: undefined,
|
|
1717
1751
|
fields: field.fields,
|
|
1718
1752
|
value: null,
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1753
|
+
component: field.itemComponent,
|
|
1754
|
+
media: field.media,
|
|
1755
|
+
storeAs: field.storeAs,
|
|
1756
|
+
reference: field.reference,
|
|
1723
1757
|
};
|
|
1724
1758
|
|
|
1725
1759
|
// The list's own path, from the FieldControl wrapping this control; an item
|
|
@@ -1734,9 +1768,20 @@ function ListControl({
|
|
|
1734
1768
|
))}
|
|
1735
1769
|
{!readOnly && canAdd ? (
|
|
1736
1770
|
<View style={{ alignSelf: "flex-start" }}>
|
|
1737
|
-
<Button
|
|
1771
|
+
<Button
|
|
1772
|
+
title="Add item"
|
|
1773
|
+
variant="default"
|
|
1774
|
+
size="sm"
|
|
1775
|
+
iconLeft="plus"
|
|
1776
|
+
onPress={addItem}
|
|
1777
|
+
disabled={!!reference && !referenceApi}
|
|
1778
|
+
testID="list-add"
|
|
1779
|
+
/>
|
|
1738
1780
|
</View>
|
|
1739
1781
|
) : null}
|
|
1782
|
+
{reference ? (
|
|
1783
|
+
<ReferencePicker visible={pickingNew} onClose={() => setPickingNew(false)} onSelect={addPicked} def={reference} />
|
|
1784
|
+
) : null}
|
|
1740
1785
|
</View>
|
|
1741
1786
|
);
|
|
1742
1787
|
}
|
|
@@ -5480,7 +5525,7 @@ export function ContentBrowser(props: ContentBrowserProps) {
|
|
|
5480
5525
|
// object store) renders media fields read-only.
|
|
5481
5526
|
return (
|
|
5482
5527
|
<MediaProvider api={props.media}>
|
|
5483
|
-
<ReferenceProvider api={props.references}>
|
|
5528
|
+
<ReferenceProvider api={props.references} onOpen={props.onOpenReference}>
|
|
5484
5529
|
{isDesktop ? <DesktopBrowser {...withSections} /> : <MobileBrowser {...withSections} />}
|
|
5485
5530
|
</ReferenceProvider>
|
|
5486
5531
|
</MediaProvider>
|
|
Binary file
|
|
@@ -61,7 +61,7 @@ export type MediaFieldProps = {
|
|
|
61
61
|
// object field and an empty string field look identical at runtime — and
|
|
62
62
|
// guessing would write the wrong shape into the document on first pick.
|
|
63
63
|
shape: "string" | "object";
|
|
64
|
-
// The media set this field browses, from the config's
|
|
64
|
+
// The media set this field browses, from the config's media block.
|
|
65
65
|
set?: string;
|
|
66
66
|
// Which form a string field writes. Ignored for the object form, which stores
|
|
67
67
|
// the asset id.
|
|
@@ -388,7 +388,7 @@ function AmbiguousNotice({
|
|
|
388
388
|
// for an image without knowing anything about the picker UI.
|
|
389
389
|
//
|
|
390
390
|
// `set` defaults to the first configured set when the field names none (a body
|
|
391
|
-
// field has no
|
|
391
|
+
// field has no media block), and the picker's own switcher covers the rest.
|
|
392
392
|
export function useFieldMedia(opts: { set?: string; storeAs?: string }): {
|
|
393
393
|
media: FieldMediaApi | undefined;
|
|
394
394
|
picker: React.ReactNode;
|
|
@@ -127,10 +127,16 @@ function NotificationRow({
|
|
|
127
127
|
item,
|
|
128
128
|
divided,
|
|
129
129
|
onPress,
|
|
130
|
+
// How much of the body a row shows. Two lines suits the bell's menu, which is
|
|
131
|
+
// a glance at what happened; the full-screen list gives it more, because the
|
|
132
|
+
// body of an error notification is the error itself and a reader who opened
|
|
133
|
+
// the list to find out what went wrong has nowhere else to read it.
|
|
134
|
+
bodyLines = 2,
|
|
130
135
|
}: {
|
|
131
136
|
item: NotificationItem;
|
|
132
137
|
divided?: boolean;
|
|
133
138
|
onPress?: () => void;
|
|
139
|
+
bodyLines?: number;
|
|
134
140
|
}) {
|
|
135
141
|
const t = useTheme();
|
|
136
142
|
const titleColor = item.level === "error" ? t.color.diffDelFg : t.color.textPrimary;
|
|
@@ -199,7 +205,9 @@ function NotificationRow({
|
|
|
199
205
|
</View>
|
|
200
206
|
<Text variant="monoSm" color="tertiary">{relativeTime(item.createdAt)}</Text>
|
|
201
207
|
</View>
|
|
202
|
-
{item.body ?
|
|
208
|
+
{item.body ? (
|
|
209
|
+
<InlineCode text={item.body} variant="monoSm" color={t.color.textSecondary} numberOfLines={bodyLines} />
|
|
210
|
+
) : null}
|
|
203
211
|
{meta ? (
|
|
204
212
|
<Text variant="monoSm" color="tertiary" numberOfLines={1}>{meta}</Text>
|
|
205
213
|
) : null}
|
|
@@ -412,6 +420,7 @@ export function NotificationList({
|
|
|
412
420
|
item={n}
|
|
413
421
|
divided={i < notifications.length - 1}
|
|
414
422
|
onPress={onPressItem ? () => onPressItem(n) : undefined}
|
|
423
|
+
bodyLines={8}
|
|
415
424
|
/>
|
|
416
425
|
))}
|
|
417
426
|
</ScrollView>
|
|
Binary file
|
package/src/index.ts
CHANGED
|
@@ -173,14 +173,15 @@ export type {
|
|
|
173
173
|
} from "./media";
|
|
174
174
|
|
|
175
175
|
// References: the document picker, "referenced by", and the host data seam
|
|
176
|
-
export { ReferenceField, ReferencePicker, ReferenceProvider, useReferenceApi } from "./components/ReferenceField";
|
|
177
|
-
export type { ReferenceFieldProps, ReferencePickerProps } from "./components/ReferenceField";
|
|
176
|
+
export { ReferenceField, ReferencePicker, ReferenceProvider, useReferenceApi, useOpenReference } from "./components/ReferenceField";
|
|
177
|
+
export type { ReferenceFieldProps, ReferencePickerProps, OpenReference } from "./components/ReferenceField";
|
|
178
178
|
export { DocumentDetails, collectSites } from "./components/DocumentDetails";
|
|
179
179
|
export type { DocumentDetailsProps, DetailsField } from "./components/DocumentDetails";
|
|
180
180
|
export { DEFAULT_REFERENCE_KEY_NAME, referenceKey, referenceValue } from "./references";
|
|
181
181
|
export type {
|
|
182
182
|
ReferenceApi,
|
|
183
183
|
ReferenceDef,
|
|
184
|
+
ReferenceDocument,
|
|
184
185
|
ReferenceEmbed,
|
|
185
186
|
ReferenceTarget,
|
|
186
187
|
ReferenceResolution,
|
package/src/references.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface ReferenceDef {
|
|
|
17
17
|
key: string;
|
|
18
18
|
// For the object shape, the child that holds the key ("ref" unless renamed).
|
|
19
19
|
keyName: string;
|
|
20
|
-
// The embedded copies, in the order
|
|
20
|
+
// The embedded copies, in the order their children are listed.
|
|
21
21
|
embed: ReferenceEmbed[];
|
|
22
22
|
// What deleting a referenced document does: restrict | unset.
|
|
23
23
|
onDelete: string;
|
|
@@ -38,6 +38,16 @@ export interface ReferenceTarget {
|
|
|
38
38
|
label: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// ReferenceDocument is what opening a document needs: enough to name it, the
|
|
42
|
+
// collection it belongs to (an open column's context), and its path (its
|
|
43
|
+
// address). A ReferenceTarget and a Referrer both satisfy it.
|
|
44
|
+
export interface ReferenceDocument {
|
|
45
|
+
id: string;
|
|
46
|
+
collection: string;
|
|
47
|
+
path: string;
|
|
48
|
+
label: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
// ReferenceResolution is the answer to "what does this stored key name?".
|
|
42
52
|
// `target` null with `ambiguous` false is a dangling reference; `ambiguous`
|
|
43
53
|
// means a field key several documents share, and `candidates` holds them.
|