@rebasepro/admin 0.12.0 → 0.12.1-canary.g009ed95
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/README.md +1 -1
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog-B-iVE9y3.js} +75 -62
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog-B-iVE9y3.js.map} +1 -1
- package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-DoLEH_bQ.js} +36 -16
- package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-DoLEH_bQ.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-C3669Ezm.js} +5 -5
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-C3669Ezm.js.map} +1 -1
- package/dist/collection_editor/serializable_types.d.ts +4 -1
- package/dist/collection_editor/ui/collection_editor/CollectionRelationsTab.d.ts +1 -0
- package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +8 -0
- package/dist/collection_editor_ui.js +5 -5
- package/dist/collection_editor_ui.js.map +1 -1
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +10 -0
- package/dist/components/CollectionViewBinding/SplitViewContext.d.ts +23 -0
- package/dist/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.d.ts +26 -0
- package/dist/components/DetailViewBinding.d.ts +5 -0
- package/dist/components/EditFormActions.d.ts +12 -1
- package/dist/components/EditViewBinding.d.ts +6 -1
- package/dist/components/EntityIdentityBar.d.ts +53 -0
- package/dist/components/EntityInspector.d.ts +29 -0
- package/dist/components/EntityPreviewNesting.d.ts +25 -0
- package/dist/components/EntityViewBinding.d.ts +9 -0
- package/dist/components/InlineEntityPreview.d.ts +47 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/editor.js +18 -10
- package/dist/editor.js.map +1 -1
- package/dist/{export-BzIpUdSK.js → export-B8DIn-u8.js} +2 -2
- package/dist/{export-BzIpUdSK.js.map → export-B8DIn-u8.js.map} +1 -1
- package/dist/form/EntityForm.d.ts +1 -1
- package/dist/form/EntityFormActions.d.ts +11 -1
- package/dist/form/PropertyFieldBinding.d.ts +1 -1
- package/dist/form/components/FieldBlock.d.ts +30 -0
- package/dist/form/components/FormRail.d.ts +18 -0
- package/dist/form/components/FormSections.d.ts +16 -0
- package/dist/form/components/index.d.ts +3 -2
- package/dist/form/components/useRailVisible.d.ts +24 -0
- package/dist/form/field_bindings/BinaryFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/DateTimeFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/VectorFieldBinding.d.ts +1 -1
- package/dist/{history-g9688IaK.js → history-ClwDEtqN.js} +32 -31
- package/dist/history-ClwDEtqN.js.map +1 -0
- package/dist/hooks/navigation/utils.d.ts +16 -0
- package/dist/hooks/useBuildSidePanel.d.ts +14 -0
- package/dist/hooks/useEntityDisplayTitle.d.ts +18 -0
- package/dist/hooks/useRecordActions.d.ts +17 -0
- package/dist/{import-BjUG-cIY.js → import-Cwzf4RW5.js} +2 -2
- package/dist/{import-BjUG-cIY.js.map → import-Cwzf4RW5.js.map} +1 -1
- package/dist/index.js +106 -35
- package/dist/index.js.map +1 -1
- package/dist/markdown-CklalUUk.js.map +1 -1
- package/dist/preview/components/InlineEntityListPreview.d.ts +15 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/property_previews/ArrayOfReferencesPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOfRelationsPreview.d.ts +1 -1
- package/dist/types/components/EntityFormActionsProps.d.ts +0 -1
- package/dist/types/components/EntityFormProps.d.ts +0 -5
- package/dist/types/fields.d.ts +24 -0
- package/dist/{util-B5fJm1FA.js → util-DSh3HRzK.js} +1974 -1130
- package/dist/util-DSh3HRzK.js.map +1 -0
- package/package.json +32 -34
- package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
- package/src/collection_editor/serializable_types.ts +4 -3
- package/src/collection_editor/serializable_utils.ts +51 -17
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +65 -22
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +38 -30
- package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +3 -1
- package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +16 -0
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +127 -15
- package/src/collection_editor/validateCollectionJson.ts +3 -3
- package/src/components/AdminModeSyncer.tsx +1 -1
- package/src/components/CollectionTableBinding/PropertyTableCell.tsx +10 -2
- package/src/components/CollectionTableBinding/column_utils.tsx +26 -2
- package/src/components/CollectionTableBinding/table_bindings.tsx +2 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +23 -16
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +7 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +8 -2
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +22 -11
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +33 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +18 -3
- package/src/components/CollectionViewBinding/SplitViewContext.tsx +34 -0
- package/src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts +48 -9
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +80 -25
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +18 -6
- package/src/components/DefaultAppBar.tsx +1 -1
- package/src/components/DefaultDrawer.tsx +1 -1
- package/src/components/DetailViewBinding.tsx +100 -143
- package/src/components/DrawerNavigationItem.tsx +1 -1
- package/src/components/EditFormActions.tsx +54 -129
- package/src/components/EditViewBinding.tsx +182 -93
- package/src/components/EntityIdentityBar.tsx +288 -0
- package/src/components/EntityInspector.tsx +132 -0
- package/src/components/EntityPreviewBinding.tsx +6 -2
- package/src/components/EntityPreviewNesting.tsx +50 -0
- package/src/components/EntityViewBinding.tsx +35 -32
- package/src/components/HomePage/FavouritesView.tsx +1 -1
- package/src/components/HomePage/NavigationCardBinding.tsx +1 -1
- package/src/components/HomePage/SmallNavigationCard.tsx +1 -1
- package/src/components/InlineEntityPreview.tsx +149 -0
- package/src/components/RebaseLayout.tsx +1 -1
- package/src/components/RebaseNavigation.tsx +45 -5
- package/src/components/RebaseRouteDefs.tsx +1 -1
- package/src/components/SelectableTable/SelectableTable.tsx +2 -2
- package/src/components/SidePanelBinding.tsx +19 -13
- package/src/components/UserSelector.tsx +12 -6
- package/src/components/common/default_entity_actions.tsx +4 -3
- package/src/components/history/EntityHistoryEntry.tsx +28 -7
- package/src/components/history/EntityHistoryView.tsx +20 -20
- package/src/components/history/LastEditedByIndicator.tsx +4 -3
- package/src/components/index.ts +2 -0
- package/src/data_import/utils/get_import_inference_type.ts +6 -0
- package/src/form/EntityForm.tsx +267 -179
- package/src/form/EntityFormActions.tsx +50 -99
- package/src/form/EntityFormBinding.tsx +4 -2
- package/src/form/PropertyFieldBinding.tsx +4 -0
- package/src/form/components/FieldBlock.tsx +119 -0
- package/src/form/components/FormRail.tsx +125 -0
- package/src/form/components/FormSections.tsx +118 -0
- package/src/form/components/index.tsx +3 -2
- package/src/form/components/useRailVisible.tsx +46 -0
- package/src/form/field_bindings/BinaryFieldBinding.tsx +3 -2
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +3 -2
- package/src/form/field_bindings/GeopointFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MapFieldBinding.tsx +1 -2
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +2 -1
- package/src/form/field_bindings/RelationFieldBinding.tsx +6 -4
- package/src/form/field_bindings/RepeatFieldBinding.tsx +4 -1
- package/src/form/field_bindings/SelectFieldBinding.tsx +3 -2
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +31 -8
- package/src/form/field_bindings/SwitchFieldBinding.tsx +7 -3
- package/src/form/field_bindings/TextFieldBinding.tsx +18 -16
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +3 -2
- package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
- package/src/hooks/navigation/useBuildUrlController.tsx +1 -1
- package/src/hooks/navigation/useTopLevelNavigation.ts +18 -7
- package/src/hooks/navigation/utils.ts +20 -0
- package/src/hooks/useBuildSideDialogsController.tsx +2 -2
- package/src/hooks/useBuildSidePanel.tsx +22 -61
- package/src/hooks/useEntityDisplayTitle.tsx +60 -0
- package/src/hooks/useHistory.ts +6 -5
- package/src/hooks/useRecordActions.tsx +69 -0
- package/src/hooks/useResolvedUsers.ts +10 -5
- package/src/preview/PropertyPreview.tsx +48 -22
- package/src/preview/components/InlineEntityListPreview.tsx +42 -0
- package/src/preview/components/ReferencePreview.tsx +34 -14
- package/src/preview/components/RelationPreview.tsx +34 -14
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +24 -1
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +25 -1
- package/src/routes/RebaseRoute.tsx +2 -2
- package/src/types/components/EntityFormActionsProps.tsx +0 -1
- package/src/types/components/EntityFormProps.tsx +0 -6
- package/src/types/fields.tsx +27 -0
- package/dist/form/components/FormEntry.d.ts +0 -6
- package/dist/form/components/FormLayout.d.ts +0 -5
- package/dist/history-g9688IaK.js.map +0 -1
- package/dist/util-B5fJm1FA.js.map +0 -1
- package/src/form/components/FormEntry.tsx +0 -22
- package/src/form/components/FormLayout.tsx +0 -16
|
@@ -2,42 +2,87 @@ import { CollectionsConfigController, SaveCollectionParams, UpdateCollectionPara
|
|
|
2
2
|
import { Properties } from "@rebasepro/types";
|
|
3
3
|
import { getSubcollections } from "@rebasepro/common";
|
|
4
4
|
|
|
5
|
-
import React, { useMemo, useRef } from "react";
|
|
5
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
6
6
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
7
|
+
import { DEFAULT_API_PATH } from "@rebasepro/app";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What the backend said about its schema editor, and how sure we are.
|
|
11
|
+
*
|
|
12
|
+
* `unknown` covers both "still asking" and "could not ask" — a backend too old
|
|
13
|
+
* to have the endpoint, or one that is unreachable. Those fall back to the
|
|
14
|
+
* build-mode guess below rather than locking the editor for someone whose
|
|
15
|
+
* editor used to work.
|
|
16
|
+
*/
|
|
17
|
+
type SchemaEditorAvailability =
|
|
18
|
+
| { state: "unknown" }
|
|
19
|
+
| { state: "known", enabled: boolean, reason?: string };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The guess this hook used to make on its own.
|
|
23
|
+
*
|
|
24
|
+
* `process.env.NODE_ENV` here is the *frontend bundle's* build mode. Whether
|
|
25
|
+
* the schema editor exists is decided by a different process entirely — the
|
|
26
|
+
* server's own `NODE_ENV`, its `collectionsDir`, whether its collections were
|
|
27
|
+
* introspected, whether `ts-morph` is installed. The two disagree in every
|
|
28
|
+
* ordinary setup: a dev frontend against a deployed API, a `baas` project, a
|
|
29
|
+
* hosted console. When they did, the editor offered itself and every save came
|
|
30
|
+
* back `404 Not Found`. It is kept only as the answer for a backend too old to
|
|
31
|
+
* be asked.
|
|
32
|
+
*/
|
|
33
|
+
const buildModeGuess = (): boolean => process.env.NODE_ENV === "production";
|
|
34
|
+
|
|
7
35
|
export function useLocalCollectionsConfigController(
|
|
8
36
|
clientOrUrl: any,
|
|
9
37
|
baseCollections: AdminCollection[] = [],
|
|
10
38
|
options?: {
|
|
11
39
|
readOnly?: boolean;
|
|
12
40
|
getAuthToken?: () => Promise<string | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Identity of the signed-in user, if any.
|
|
43
|
+
*
|
|
44
|
+
* Only used to re-ask the backend when it changes: whether the editor
|
|
45
|
+
* will accept a write depends on who is asking, and the answer to an
|
|
46
|
+
* anonymous probe does not survive a sign-in.
|
|
47
|
+
*/
|
|
48
|
+
authKey?: string | null;
|
|
13
49
|
}
|
|
14
50
|
): CollectionsConfigController {
|
|
15
51
|
|
|
16
52
|
const parsedCollections = baseCollections;
|
|
17
|
-
|
|
53
|
+
|
|
18
54
|
// Store latest options in a ref to prevent stale closures in the `request` function
|
|
19
55
|
// due to useMemo caching the saveCollection function.
|
|
20
56
|
const optionsRef = useRef(options);
|
|
21
57
|
optionsRef.current = options;
|
|
22
58
|
|
|
59
|
+
// `/api` only by default — a backend configured with another `basePath`
|
|
60
|
+
// mounts the schema editor under that instead.
|
|
61
|
+
const clientBaseUrl = typeof clientOrUrl === "string"
|
|
62
|
+
? clientOrUrl
|
|
63
|
+
: (clientOrUrl?.baseUrl ?? "");
|
|
64
|
+
const clientApiPath = (typeof clientOrUrl === "object" && clientOrUrl !== null && clientOrUrl.apiPath)
|
|
65
|
+
|| DEFAULT_API_PATH;
|
|
66
|
+
const editorUrl = `${String(clientBaseUrl).replace(/\/$/, "")}${clientApiPath}/schema-editor`;
|
|
67
|
+
|
|
68
|
+
const resolveToken = async (): Promise<string | null> => {
|
|
69
|
+
let token = optionsRef.current?.getAuthToken ? await optionsRef.current.getAuthToken() : null;
|
|
70
|
+
if (!token && typeof clientOrUrl === "object" && clientOrUrl !== null && clientOrUrl.resolveToken) {
|
|
71
|
+
token = await clientOrUrl.resolveToken();
|
|
72
|
+
}
|
|
73
|
+
return token ?? null;
|
|
74
|
+
};
|
|
75
|
+
|
|
23
76
|
const request = async (endpoint: string, payload: Record<string, unknown>) => {
|
|
24
77
|
try {
|
|
25
|
-
|
|
26
|
-
let baseUrl = typeof clientOrUrl === "string" ? clientOrUrl : "";
|
|
27
|
-
|
|
28
|
-
if (typeof clientOrUrl === "object" && clientOrUrl !== null) {
|
|
29
|
-
baseUrl = clientOrUrl.baseUrl || baseUrl;
|
|
30
|
-
if (!token && clientOrUrl.resolveToken) {
|
|
31
|
-
token = await clientOrUrl.resolveToken();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
78
|
+
const token = await resolveToken();
|
|
34
79
|
|
|
35
80
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
36
81
|
if (token) {
|
|
37
82
|
headers["Authorization"] = `Bearer ${token}`;
|
|
38
83
|
}
|
|
39
84
|
|
|
40
|
-
const response = await fetch(`${
|
|
85
|
+
const response = await fetch(`${editorUrl}${endpoint}`, {
|
|
41
86
|
method: "POST",
|
|
42
87
|
headers,
|
|
43
88
|
body: JSON.stringify(payload)
|
|
@@ -67,10 +112,77 @@ export function useLocalCollectionsConfigController(
|
|
|
67
112
|
}
|
|
68
113
|
};
|
|
69
114
|
|
|
115
|
+
// ── Ask the backend whether it will accept an edit ────────────────
|
|
116
|
+
const forcedReadOnly = options?.readOnly;
|
|
117
|
+
const authKey = options?.authKey ?? null;
|
|
118
|
+
const [availability, setAvailability] = useState<SchemaEditorAvailability>({ state: "unknown" });
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
// Nothing to ask about: the caller has already decided.
|
|
122
|
+
if (forcedReadOnly === true) return;
|
|
123
|
+
|
|
124
|
+
let cancelled = false;
|
|
125
|
+
(async () => {
|
|
126
|
+
try {
|
|
127
|
+
const token = await resolveToken();
|
|
128
|
+
const response = await fetch(`${editorUrl}/status`, {
|
|
129
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
130
|
+
});
|
|
131
|
+
if (cancelled) return;
|
|
132
|
+
|
|
133
|
+
// A backend from before the endpoint existed. Guessing from the
|
|
134
|
+
// bundle's build mode is wrong, but it is what that backend's
|
|
135
|
+
// admin panel has always done, and locking the editor for
|
|
136
|
+
// someone whose editor works would be worse.
|
|
137
|
+
if (response.status === 404) {
|
|
138
|
+
setAvailability({ state: "unknown" });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const body = await response.json().catch(() => ({})) as {
|
|
143
|
+
enabled?: boolean,
|
|
144
|
+
reason?: string,
|
|
145
|
+
error?: { message?: string }
|
|
146
|
+
};
|
|
147
|
+
if (cancelled) return;
|
|
148
|
+
|
|
149
|
+
if (response.ok) {
|
|
150
|
+
setAvailability({
|
|
151
|
+
state: "known",
|
|
152
|
+
enabled: body.enabled === true,
|
|
153
|
+
reason: body.reason
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
// 401/403 (not an admin) or 501 (no auth configured at all)
|
|
157
|
+
// — all of them mean this session cannot edit collections,
|
|
158
|
+
// and the body says why better than we could.
|
|
159
|
+
setAvailability({
|
|
160
|
+
state: "known",
|
|
161
|
+
enabled: false,
|
|
162
|
+
reason: body.error?.message
|
|
163
|
+
?? `The backend refused to say whether collections are editable (HTTP ${response.status}).`
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
if (!cancelled) setAvailability({ state: "unknown" });
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
170
|
+
|
|
171
|
+
return () => { cancelled = true; };
|
|
172
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
173
|
+
}, [editorUrl, forcedReadOnly, authKey]);
|
|
174
|
+
|
|
175
|
+
const readOnly = forcedReadOnly
|
|
176
|
+
?? (availability.state === "known" ? !availability.enabled : buildModeGuess());
|
|
177
|
+
|
|
178
|
+
const readOnlyReason = availability.state === "known" && availability.reason
|
|
179
|
+
? availability.reason
|
|
180
|
+
: "Collections can only be edited against a backend running the schema editor, which is off in production.";
|
|
181
|
+
|
|
70
182
|
return useMemo(() => ({
|
|
71
183
|
loading: false,
|
|
72
|
-
readOnly
|
|
73
|
-
readOnlyReason
|
|
184
|
+
readOnly,
|
|
185
|
+
readOnlyReason,
|
|
74
186
|
collections: parsedCollections,
|
|
75
187
|
getCollection: (id: string) => {
|
|
76
188
|
const found = parsedCollections.find(c => (c as AdminCollection & { id?: string }).id === id || c.slug === id);
|
|
@@ -119,5 +231,5 @@ collectionData: { propertiesOrder: newPropertiesOrder } });
|
|
|
119
231
|
|
|
120
232
|
navigationEntries: [],
|
|
121
233
|
saveNavigationEntries: async () => { }
|
|
122
|
-
}), [clientOrUrl, parsedCollections,
|
|
234
|
+
}), [clientOrUrl, parsedCollections, readOnly, readOnlyReason, options?.getAuthToken]);
|
|
123
235
|
}
|
|
@@ -135,12 +135,12 @@ function validateProperty(
|
|
|
135
135
|
// silently honoured by whichever consumer read it first.
|
|
136
136
|
const allowedByKind: Record<string, string[]> = {
|
|
137
137
|
belongsTo: ["localKey"],
|
|
138
|
-
hasOne: ["foreignKeyOnTarget"],
|
|
139
|
-
hasMany: ["foreignKeyOnTarget"],
|
|
138
|
+
hasOne: ["foreignKeyOnTarget", "sourceKey"],
|
|
139
|
+
hasMany: ["foreignKeyOnTarget", "sourceKey"],
|
|
140
140
|
manyToMany: ["through"],
|
|
141
141
|
via: ["joinPath", "cardinality"]
|
|
142
142
|
};
|
|
143
|
-
const linkFields = ["localKey", "foreignKeyOnTarget", "through", "joinPath", "cardinality"];
|
|
143
|
+
const linkFields = ["localKey", "foreignKeyOnTarget", "sourceKey", "through", "joinPath", "cardinality"];
|
|
144
144
|
const allowed = allowedByKind[String(link.kind)] ?? [];
|
|
145
145
|
for (const field of linkFields) {
|
|
146
146
|
if (link[field] !== undefined && !allowed.includes(field)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AppView } from "@rebasepro/admin-types";
|
|
2
2
|
import React, { useEffect } from "react";
|
|
3
|
-
import { useLocation } from "react-router
|
|
3
|
+
import { useLocation } from "react-router";
|
|
4
4
|
;
|
|
5
5
|
import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
|
|
6
6
|
import { useAdminModeController } from "@rebasepro/app";
|
|
@@ -205,6 +205,16 @@ export const PropertyTableCell = React.memo<PropertyTableCellProps<any>>(
|
|
|
205
205
|
}
|
|
206
206
|
};
|
|
207
207
|
|
|
208
|
+
// `getTableBindingForProperty` builds its `Component` as a fresh inline
|
|
209
|
+
// arrow function on every call. Calling it during render therefore gave
|
|
210
|
+
// React a new component *type* each pass, so the cell was unmounted and
|
|
211
|
+
// remounted instead of updated — which is why a BooleanSwitch in the
|
|
212
|
+
// table never animated (a replaced element cannot run a CSS transition)
|
|
213
|
+
// and why every cell re-created its subtree on unrelated renders.
|
|
214
|
+
const tableBinding = useMemo(
|
|
215
|
+
() => getTableBindingForProperty(property, selected),
|
|
216
|
+
[property, selected]);
|
|
217
|
+
|
|
208
218
|
let innerComponent: React.ReactNode | undefined;
|
|
209
219
|
let allowScroll = false;
|
|
210
220
|
let showExpandIcon = false;
|
|
@@ -243,8 +253,6 @@ export const PropertyTableCell = React.memo<PropertyTableCellProps<any>>(
|
|
|
243
253
|
}
|
|
244
254
|
|
|
245
255
|
if (!customField && (!customPreview || selected)) {
|
|
246
|
-
const tableBinding = getTableBindingForProperty(property, selected);
|
|
247
|
-
|
|
248
256
|
if (tableBinding) {
|
|
249
257
|
const Component = tableBinding.Component;
|
|
250
258
|
innerComponent = <Component
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Properties, Property } from "@rebasepro/types";
|
|
1
|
+
import type { Properties, Property, RelationProperty } from "@rebasepro/types";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { getTableCellAlignment, getTablePropertyColumnWidth } from "./internal/common";
|
|
4
4
|
import { FilterValues } from "@rebasepro/types";
|
|
@@ -52,7 +52,7 @@ export function propertiesToColumns<M extends Record<string, unknown>>({ propert
|
|
|
52
52
|
align: getTableCellAlignment(property),
|
|
53
53
|
icon: getIconForProperty(property, "small"),
|
|
54
54
|
title: property.name ?? key as string,
|
|
55
|
-
sortable: sortable,
|
|
55
|
+
sortable: sortable && sortableProperty(property),
|
|
56
56
|
filter: !disabledFilter && filterable,
|
|
57
57
|
width: getTablePropertyColumnWidth(property),
|
|
58
58
|
resizable: true,
|
|
@@ -67,6 +67,30 @@ export function propertiesToColumns<M extends Record<string, unknown>>({ propert
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Whether this column can be ordered by.
|
|
72
|
+
*
|
|
73
|
+
* `sortable` arrived as one flag for the whole table while `filter` was
|
|
74
|
+
* computed per property, so every column advertised a sort — including the ones
|
|
75
|
+
* no `ORDER BY` can be written for. A to-many relation is a *set* per row
|
|
76
|
+
* (`posts.tags`), and there is no single value to order on; the driver used to
|
|
77
|
+
* answer by silently dropping the sort and returning rows in whatever order
|
|
78
|
+
* Postgres pleased, which is how a header that did nothing went unnoticed.
|
|
79
|
+
*
|
|
80
|
+
* Now that the driver refuses instead, offering the control would turn a
|
|
81
|
+
* useless header into a failed request. Same authority as the filter side: ask
|
|
82
|
+
* the property, not the table.
|
|
83
|
+
*/
|
|
84
|
+
function sortableProperty(property: Property): boolean {
|
|
85
|
+
if (property.type !== "relation") return true;
|
|
86
|
+
const relationProperty = property as RelationProperty;
|
|
87
|
+
const kind = relationProperty.relation?.kind ?? relationProperty.resolvedRelation?.kind;
|
|
88
|
+
// An unresolved relation keeps the benefit of the doubt — the same choice
|
|
89
|
+
// `isFilterableRelation` makes for a kind it cannot see.
|
|
90
|
+
if (!kind) return true;
|
|
91
|
+
return kind === "belongsTo";
|
|
92
|
+
}
|
|
93
|
+
|
|
70
94
|
function filterableProperty(property: Property, partOfArray = false, engine?: string): boolean {
|
|
71
95
|
// A relation this engine's driver cannot compile into a `WHERE` has
|
|
72
96
|
// nothing to filter on, so the header's filter control would open onto a
|
|
@@ -189,13 +189,14 @@ export function getTableBindingForProperty(
|
|
|
189
189
|
};
|
|
190
190
|
} else if (property.type === "date") {
|
|
191
191
|
return {
|
|
192
|
-
Component: ({ propertyKey, error, validationError, disabled, selected, property, internalValue, updateValue }: TableFieldBindingProps) => {
|
|
192
|
+
Component: ({ propertyKey, error, validationError, disabled, selected, size, property, internalValue, updateValue }: TableFieldBindingProps) => {
|
|
193
193
|
const { locale } = useCustomizationController();
|
|
194
194
|
return (
|
|
195
195
|
<VirtualTableDateField
|
|
196
196
|
name={propertyKey}
|
|
197
197
|
error={validationError ?? error}
|
|
198
198
|
disabled={disabled}
|
|
199
|
+
small={getPreviewSizeFrom(size) !== "medium"}
|
|
199
200
|
mode={(property as DateProperty).mode}
|
|
200
201
|
focused={selected}
|
|
201
202
|
internalValue={internalValue as Date}
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
import { useAnalyticsController } from "@rebasepro/app";
|
|
39
39
|
import { setIn } from "@rebasepro/forms";
|
|
40
40
|
import { useBoardDataController } from "./useBoardDataController";
|
|
41
|
-
import { useKanbanDragAndDrop } from "./hooks/useKanbanDragAndDrop";
|
|
41
|
+
import { isValidOrderKey, ORDER_KEY_DIGITS, useKanbanDragAndDrop } from "./hooks/useKanbanDragAndDrop";
|
|
42
42
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
|
43
43
|
import { generateNKeysBetween } from "fractional-indexing";
|
|
44
44
|
|
|
@@ -298,11 +298,11 @@ parentEntityIds,
|
|
|
298
298
|
if (!orderProperty || dataLoading) return false;
|
|
299
299
|
// Use collection-level count detection
|
|
300
300
|
if (missingOrderCount > 0) return true;
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
301
|
+
// A value that is present but is not a fractional-indexing key is no
|
|
302
|
+
// more usable than a missing one — dropping a card next to it cannot
|
|
303
|
+
// produce a key between the two — so it earns the same offer to
|
|
304
|
+
// initialise the column.
|
|
305
|
+
return allEntities.some((entity: Entity<M>) => !isValidOrderKey(entity.values?.[orderProperty]));
|
|
306
306
|
}, [allEntities, orderProperty, dataLoading, missingOrderCount]);
|
|
307
307
|
|
|
308
308
|
// Create a lookup map of entity ID → column from boardDataController data
|
|
@@ -337,7 +337,8 @@ parentEntityIds,
|
|
|
337
337
|
loading: colData?.loading ?? true,
|
|
338
338
|
hasMore: colData?.hasMore ?? false,
|
|
339
339
|
itemCount: colData?.entities?.length ?? 0,
|
|
340
|
-
totalCount: colData?.totalCount
|
|
340
|
+
totalCount: colData?.totalCount,
|
|
341
|
+
error: colData?.error
|
|
341
342
|
};
|
|
342
343
|
});
|
|
343
344
|
return state;
|
|
@@ -375,20 +376,24 @@ parentEntityIds,
|
|
|
375
376
|
setBackfillLoading(true);
|
|
376
377
|
|
|
377
378
|
try {
|
|
378
|
-
// Fetch ALL documents from collection (not relying on loaded
|
|
379
|
+
// Fetch ALL documents from collection (not relying on loaded
|
|
380
|
+
// entities). No `orderBy`: with no usable order key there is
|
|
381
|
+
// nothing to order by, and this is the same query the columns
|
|
382
|
+
// themselves run, so the keys are handed out in the order the
|
|
383
|
+
// board is showing — which is what the dialog promises.
|
|
379
384
|
const allDocsRes = await dataClient.collection(fullPath).find({
|
|
380
385
|
limit: 10000 // Fetch all
|
|
381
386
|
});
|
|
382
387
|
const allDocs = allDocsRes.data as Entity<M>[];
|
|
383
388
|
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
+
// Entities without a usable order key. Testing only for null here
|
|
390
|
+
// meant a column full of unusable values — `"12"`, `"6"` — offered
|
|
391
|
+
// an Initialize button that updated nothing and never went away.
|
|
392
|
+
const entitiesToUpdate = allDocs.filter((entity: Entity<M>) =>
|
|
393
|
+
!isValidOrderKey(entity.values?.[orderProperty]));
|
|
389
394
|
|
|
390
395
|
// Generate string fractional keys for all entities that need them
|
|
391
|
-
const keys = generateNKeysBetween(null, null, entitiesToUpdate.length);
|
|
396
|
+
const keys = generateNKeysBetween(null, null, entitiesToUpdate.length, ORDER_KEY_DIGITS);
|
|
392
397
|
const updates: Promise<void>[] = [];
|
|
393
398
|
entitiesToUpdate.forEach((entity: Entity<M>, index: number) => {
|
|
394
399
|
const updatedValues = setIn({ ...entity.values }, orderProperty, keys[index]);
|
|
@@ -591,8 +596,10 @@ parentEntityIds,
|
|
|
591
596
|
</div>
|
|
592
597
|
)}
|
|
593
598
|
|
|
594
|
-
{/* Main board
|
|
595
|
-
|
|
599
|
+
{/* Main board. It scrolls its own columns vertically and its own
|
|
600
|
+
row horizontally, so an `overflow-auto` here only added a second
|
|
601
|
+
scroll container around both. */}
|
|
602
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
596
603
|
<KanbanView
|
|
597
604
|
data={boardItems}
|
|
598
605
|
columns={columns}
|
|
@@ -113,8 +113,14 @@ export function CollectionViewActions<M extends Record<string, unknown>>({
|
|
|
113
113
|
|
|
114
114
|
const multipleDeleteEnabled = canDelete(collection, path, null);
|
|
115
115
|
|
|
116
|
+
// Only once something is selected. A permanently-disabled bin sat in every
|
|
117
|
+
// collection toolbar — and in the split view, where the toolbar is a narrow
|
|
118
|
+
// strip above the list, it was a third of the visible controls doing
|
|
119
|
+
// nothing. It appears the moment you tick a row, which is when you want it.
|
|
120
|
+
const hasSelection = Boolean(selectedEntities?.length);
|
|
121
|
+
|
|
116
122
|
let multipleDeleteButton: React.ReactNode | undefined;
|
|
117
|
-
{
|
|
123
|
+
if (hasSelection) {
|
|
118
124
|
const button = largeLayout && !compact
|
|
119
125
|
? <Button
|
|
120
126
|
variant={"text"}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
ViewMode
|
|
14
14
|
} from "@rebasepro/admin-types";
|
|
15
15
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
16
|
-
import { useSearchParams } from "react-router
|
|
16
|
+
import { useSearchParams } from "react-router";
|
|
17
17
|
|
|
18
18
|
import { deepEqual as equal } from "fast-equals"
|
|
19
19
|
import { CollectionRowActions, CollectionTableBinding } from "../CollectionTableBinding";
|
|
@@ -1166,8 +1166,14 @@ parentEntityIds,
|
|
|
1166
1166
|
) : (
|
|
1167
1167
|
<div className="flex flex-col w-full h-full">
|
|
1168
1168
|
{toolbarNode}
|
|
1169
|
+
{/* `min-h-0` is what lets the board scroll its columns.
|
|
1170
|
+
A flex item defaults to `min-height: auto`, so this
|
|
1171
|
+
one grew to the board's full content height — 1230px
|
|
1172
|
+
in a 883px area — and the ancestor's `overflow-hidden`
|
|
1173
|
+
simply cut the rest off, with each column's own
|
|
1174
|
+
scroller never reaching its limit. */}
|
|
1169
1175
|
<div className={cls(
|
|
1170
|
-
"flex-1 flex flex-col",
|
|
1176
|
+
"flex-1 min-h-0 flex flex-col",
|
|
1171
1177
|
(viewMode === "list" || viewMode === "cards") && "overflow-y-auto"
|
|
1172
1178
|
)}>
|
|
1173
1179
|
{collectionSurface}
|
|
@@ -4,15 +4,16 @@ import React, { useState, useCallback } from "react";
|
|
|
4
4
|
import { useAuthController, useLargeLayout, useTranslation, useSlot } from "@rebasepro/app";
|
|
5
5
|
import { CollectionActionsProps, EntityTableController, SelectionController, AdminCollection } from "@rebasepro/admin-types";
|
|
6
6
|
import { ErrorBoundary, iconSize } from "@rebasepro/ui";
|
|
7
|
-
import {
|
|
7
|
+
import { Badge, Button, cls, FilterIcon, IconButton, Tooltip, XIcon } from "@rebasepro/ui";
|
|
8
8
|
import { ClearFilterSortButton } from "../ClearFilterSortButton";
|
|
9
9
|
import { FiltersDialog } from "./FiltersDialog";
|
|
10
10
|
import { FilterPresetsButton } from "./FilterPresetsButton";
|
|
11
11
|
import { toArray } from "@rebasepro/utils";
|
|
12
|
-
import { useNavigate } from "react-router
|
|
12
|
+
import { useNavigate } from "react-router";
|
|
13
13
|
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
14
14
|
import { useAdminContext } from "../../hooks/useAdminContext";
|
|
15
15
|
import { withViewMode } from "../../util/view_mode";
|
|
16
|
+
import { useSplitView } from "./SplitViewContext";
|
|
16
17
|
|
|
17
18
|
export type CollectionViewStartActionsProps<M extends Record<string, unknown>> = {
|
|
18
19
|
collection: AdminCollection<M>;
|
|
@@ -49,6 +50,7 @@ export function CollectionViewStartActions<M extends Record<string, unknown>>({
|
|
|
49
50
|
|
|
50
51
|
const navigate = useNavigate();
|
|
51
52
|
const urlController = useUrlController();
|
|
53
|
+
const splitView = useSplitView();
|
|
52
54
|
|
|
53
55
|
// Filters dialog state
|
|
54
56
|
const [filtersDialogOpen, setFiltersDialogOpen] = useState(false);
|
|
@@ -77,14 +79,20 @@ parentEntityIds,
|
|
|
77
79
|
navigate(withViewMode(urlController.buildUrlCollectionPath(path)));
|
|
78
80
|
}, [navigate, urlController, path]);
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
// In a split view the control that closes the list lives at the leading
|
|
83
|
+
// edge of the record's app bar (`SplitListCloseButton`), not here.
|
|
84
|
+
//
|
|
85
|
+
// Outside one — a compact list with no record open beside it — the only
|
|
86
|
+
// thing this can mean is "leave the record behind".
|
|
87
|
+
const backButton = compact && !splitView?.detailOpen && (
|
|
88
|
+
<Tooltip title={t("close")} key={"close_tooltip"}>
|
|
82
89
|
<IconButton
|
|
83
90
|
size="small"
|
|
84
91
|
onClick={handleBackClick}
|
|
92
|
+
aria-label={t("close")}
|
|
85
93
|
className="mr-1"
|
|
86
94
|
>
|
|
87
|
-
<
|
|
95
|
+
<XIcon size={iconSize.smallest}/>
|
|
88
96
|
</IconButton>
|
|
89
97
|
</Tooltip>
|
|
90
98
|
);
|
|
@@ -107,20 +115,23 @@ parentEntityIds,
|
|
|
107
115
|
{activeFilterCount > 0 ? `(${activeFilterCount})` : t("filters")}
|
|
108
116
|
</Button>
|
|
109
117
|
) : (
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
size="small"
|
|
118
|
+
<IconButton
|
|
119
|
+
size={"small"}
|
|
113
120
|
onClick={() => setFiltersDialogOpen(true)}
|
|
114
121
|
className={cls(activeFilterCount > 0 && "text-primary")}
|
|
115
122
|
>
|
|
116
|
-
<FilterIcon size={iconSize.
|
|
117
|
-
</
|
|
123
|
+
<FilterIcon size={iconSize.smallest}/>
|
|
124
|
+
</IconButton>
|
|
118
125
|
)}
|
|
119
126
|
</Badge>
|
|
120
127
|
</Tooltip>
|
|
121
128
|
);
|
|
122
129
|
|
|
123
|
-
|
|
130
|
+
// Not in the split view. The toolbar there is a strip a few hundred pixels
|
|
131
|
+
// wide above the list, and a row of preset chips crowded out the controls
|
|
132
|
+
// that act on what you are looking at. The presets are still one click away
|
|
133
|
+
// inside the filters dialog.
|
|
134
|
+
const filterPresetsButton = !compact && collection.filterPresets?.length ? (
|
|
124
135
|
<FilterPresetsButton
|
|
125
136
|
key={"filter_presets"}
|
|
126
137
|
filterPresets={collection.filterPresets}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { IconButton, iconSize, Tooltip, XIcon } from "@rebasepro/ui";
|
|
4
|
+
import { useTranslation } from "@rebasepro/app";
|
|
5
|
+
|
|
6
|
+
import { useSplitView } from "./SplitViewContext";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Closes the list of a split view, leaving the open record on its own.
|
|
10
|
+
*
|
|
11
|
+
* It leads the record's own app bar, where the breadcrumb starts — this is the
|
|
12
|
+
* entity pane's old "Open full screen" button, moved to that edge and wearing an
|
|
13
|
+
* X: closing the list and opening the record full screen are the same thing, so
|
|
14
|
+
* there is one control and one route (`#full`) rather than two.
|
|
15
|
+
*/
|
|
16
|
+
export function SplitListCloseButton() {
|
|
17
|
+
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
const splitView = useSplitView();
|
|
20
|
+
|
|
21
|
+
if (!splitView?.detailOpen) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Tooltip title={t("hide_list")}>
|
|
25
|
+
<IconButton
|
|
26
|
+
size="small"
|
|
27
|
+
onClick={splitView.openFullScreen}
|
|
28
|
+
aria-label={t("hide_list")}>
|
|
29
|
+
<XIcon size={iconSize.smallest}/>
|
|
30
|
+
</IconButton>
|
|
31
|
+
</Tooltip>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -11,10 +11,11 @@ import {
|
|
|
11
11
|
} from "@rebasepro/ui";
|
|
12
12
|
import { useLargeLayout, UnsavedChangesDialog, useNavigationBlocker } from "@rebasepro/app";
|
|
13
13
|
import { useCollectionRegistryController } from "../../hooks/navigation/contexts/CollectionRegistryContext";
|
|
14
|
-
import { useNavigate, useLocation } from "react-router
|
|
14
|
+
import { useNavigate, useLocation } from "react-router";
|
|
15
15
|
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
16
16
|
import { ErrorBoundary } from "@rebasepro/ui";
|
|
17
17
|
import { withViewMode } from "../../util/view_mode";
|
|
18
|
+
import { SplitViewProvider } from "./SplitViewContext";
|
|
18
19
|
|
|
19
20
|
export type SplitListViewProps<M extends Record<string, unknown> = Record<string, unknown>> = {
|
|
20
21
|
collection: AdminCollection<M>;
|
|
@@ -408,8 +409,22 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
408
409
|
</div>
|
|
409
410
|
) : <></>;
|
|
410
411
|
|
|
412
|
+
// Closing the list is the full-screen entity route: the same `#full` the
|
|
413
|
+
// entity pane's expand button used, so there is one way to give the record
|
|
414
|
+
// the whole window and one way back out of it.
|
|
415
|
+
const openFullScreen = useCallback(() => {
|
|
416
|
+
if (selectedEntityId === undefined) return;
|
|
417
|
+
const suffix = isEditMode ? "/edit" : "";
|
|
418
|
+
navigate(`${buildUrl(`${path}/${selectedEntityId}${suffix}`)}#full`);
|
|
419
|
+
}, [navigate, buildUrl, path, selectedEntityId, isEditMode]);
|
|
420
|
+
|
|
421
|
+
const splitViewController = useMemo(() => ({
|
|
422
|
+
detailOpen: selectedEntityId !== undefined,
|
|
423
|
+
openFullScreen
|
|
424
|
+
}), [selectedEntityId, openFullScreen]);
|
|
425
|
+
|
|
411
426
|
return (
|
|
412
|
-
|
|
427
|
+
<SplitViewProvider value={splitViewController}>
|
|
413
428
|
<ResizablePanels
|
|
414
429
|
stacked={!largeLayout}
|
|
415
430
|
firstPanel={listPanel}
|
|
@@ -428,6 +443,6 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
428
443
|
}}
|
|
429
444
|
handleCancel={() => blocker?.reset?.()}
|
|
430
445
|
body={<>You have unsaved changes in this <b>{collection.singularName ?? collection.name}</b>.</>}/>
|
|
431
|
-
|
|
446
|
+
</SplitViewProvider>
|
|
432
447
|
);
|
|
433
448
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React, { useContext } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The enclosing master-detail split view, exposed to the list panel so it can
|
|
5
|
+
* carry the control that closes it.
|
|
6
|
+
*/
|
|
7
|
+
export type SplitViewController = {
|
|
8
|
+
/** Whether a record is open in the detail pane. */
|
|
9
|
+
detailOpen: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Close the list and leave the open record on its own, full screen. This is
|
|
12
|
+
* the existing full-screen entity route (`#full`), not a second mechanism.
|
|
13
|
+
*/
|
|
14
|
+
openFullScreen: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const SplitViewContext = React.createContext<SplitViewController | undefined>(undefined);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The enclosing split view, or `undefined` when the component is not rendered
|
|
21
|
+
* inside one (a side panel, a dialog, a full-screen route).
|
|
22
|
+
*/
|
|
23
|
+
export function useSplitView(): SplitViewController | undefined {
|
|
24
|
+
return useContext(SplitViewContext);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function SplitViewProvider({
|
|
28
|
+
value,
|
|
29
|
+
children
|
|
30
|
+
}: { value: SplitViewController, children: React.ReactNode }) {
|
|
31
|
+
return <SplitViewContext.Provider value={value}>
|
|
32
|
+
{children}
|
|
33
|
+
</SplitViewContext.Provider>;
|
|
34
|
+
}
|