@rebasepro/admin 0.12.1-canary.gf5f1d39 → 0.13.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/README.md +1 -1
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog-CjXkWZ64.js} +75 -62
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog-CjXkWZ64.js.map} +1 -1
- package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-W8I3YV89.js} +36 -16
- package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-W8I3YV89.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-BsLTNoqX.js} +5 -5
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-BsLTNoqX.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/CollectionTableBinding/column_utils.d.ts +17 -0
- package/dist/components/CollectionViewBinding/CollectionViewStartActions.d.ts +7 -2
- package/dist/components/CollectionViewBinding/SortButton.d.ts +24 -0
- 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 +35 -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/history/EntityHistoryView.d.ts +4 -1
- 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-J79rMoNj.js} +2 -2
- package/dist/{export-BzIpUdSK.js.map → export-J79rMoNj.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-D7L_JlLO.js} +35 -33
- package/dist/history-D7L_JlLO.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/useHistory.d.ts +7 -0
- package/dist/hooks/useRecordActions.d.ts +17 -0
- package/dist/{import-BjUG-cIY.js → import-BvYl--U4.js} +2 -2
- package/dist/{import-BjUG-cIY.js.map → import-BvYl--U4.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-GiwPgxnL.js} +2106 -1117
- package/dist/util-GiwPgxnL.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 +59 -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 +9 -2
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +47 -12
- package/src/components/CollectionViewBinding/SortButton.tsx +189 -0
- 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 +112 -148
- package/src/components/DrawerNavigationItem.tsx +1 -1
- package/src/components/EditFormActions.tsx +54 -129
- package/src/components/EditViewBinding.tsx +202 -99
- package/src/components/EntityIdentityBar.tsx +288 -0
- package/src/components/EntityInspector.tsx +137 -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 +29 -24
- 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 +20 -10
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import React, { useEffect, useState, useCallback } from "react";
|
|
3
3
|
;
|
|
4
|
-
import { useApiConfig } from "@rebasepro/app";
|
|
4
|
+
import { useApiBase, useApiConfig } from "@rebasepro/app";
|
|
5
5
|
import { useAuthController } from "@rebasepro/app";
|
|
6
6
|
import { HistoryEntryData } from "../../hooks";
|
|
7
7
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
@@ -35,6 +35,7 @@ export function LastEditedByIndicator({
|
|
|
35
35
|
collection: AdminCollection;
|
|
36
36
|
}) {
|
|
37
37
|
const apiConfig = useApiConfig();
|
|
38
|
+
const apiBase = useApiBase();
|
|
38
39
|
const authController = useAuthController();
|
|
39
40
|
const [latestEntry, setLatestEntry] = useState<HistoryEntryData | undefined>();
|
|
40
41
|
|
|
@@ -46,7 +47,7 @@ export function LastEditedByIndicator({
|
|
|
46
47
|
const headers: Record<string, string> = {};
|
|
47
48
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
48
49
|
|
|
49
|
-
const url = `${
|
|
50
|
+
const url = `${apiBase}/data/${collection.slug}/${entityId}/history?limit=1&offset=0`;
|
|
50
51
|
const response = await fetch(url, { headers });
|
|
51
52
|
|
|
52
53
|
if (response.ok) {
|
|
@@ -59,7 +60,7 @@ export function LastEditedByIndicator({
|
|
|
59
60
|
// Silently fail — this is a non-critical UI indicator
|
|
60
61
|
console.debug("Failed to fetch latest history entry:", error);
|
|
61
62
|
}
|
|
62
|
-
}, [apiConfig, entityId, collection.slug]);
|
|
63
|
+
}, [apiConfig, apiBase, entityId, collection.slug]);
|
|
63
64
|
|
|
64
65
|
useEffect(() => {
|
|
65
66
|
fetchLatest();
|
package/src/components/index.ts
CHANGED
|
@@ -25,6 +25,8 @@ export * from "./ReferenceWidget";
|
|
|
25
25
|
export * from "./SearchIconsView";
|
|
26
26
|
export * from "./FieldCaption";
|
|
27
27
|
export * from "./EntityPreviewBinding";
|
|
28
|
+
export * from "./EntityPreviewNesting";
|
|
29
|
+
export * from "./InlineEntityPreview";
|
|
28
30
|
|
|
29
31
|
// history is lazy-loaded by EditViewBinding and resolutions.ts
|
|
30
32
|
// export * from "./history";
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { DataType } from "@rebasepro/types";
|
|
2
2
|
|
|
3
3
|
export function getInferenceType(value: any): DataType {
|
|
4
|
+
// `typeof null === "object"`, so a null or empty CSV cell used to fall past
|
|
5
|
+
// every check and land on "map" — an empty cell described as a nested object.
|
|
6
|
+
// `inferTypeFromValue` in @rebasepro/inference already answers "string" here;
|
|
7
|
+
// the two entry points into inference have to agree.
|
|
8
|
+
if (value === null || value === undefined)
|
|
9
|
+
return "string";
|
|
4
10
|
if (typeof value === "number")
|
|
5
11
|
return "number";
|
|
6
12
|
else if (typeof value === "string")
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
|
|
2
|
-
import type {
|
|
2
|
+
import type { AdditionalFieldDelegateProps } from "@rebasepro/admin-types";
|
|
3
3
|
import type { FormContext, PropertyFieldBindingProps } from "../types/fields";
|
|
4
|
-
import type { PropertyConfig } from "@rebasepro/admin-types";
|
|
5
4
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
5
|
import { Entity, EntityStatus, EntityValues } from "@rebasepro/types";
|
|
7
|
-
import type { EntityFormProps
|
|
6
|
+
import type { EntityFormProps } from "../types/components/EntityFormProps";
|
|
8
7
|
import { deepEqual as equal } from "fast-equals";
|
|
9
8
|
|
|
10
9
|
import { ErrorBoundary } from "@rebasepro/ui";
|
|
11
|
-
import { AlignLeftIcon,
|
|
10
|
+
import { AlignLeftIcon, useDebouncedCallback } from "@rebasepro/ui";
|
|
12
11
|
import { getDefaultValuesFor } from "@rebasepro/common";
|
|
13
|
-
import {
|
|
12
|
+
import { isReadOnly } from "@rebasepro/app";
|
|
14
13
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { Alert, Button,
|
|
14
|
+
import { getFormFieldKeys, resolveFormLayout } from "@rebasepro/app";
|
|
15
|
+
import type { ResolvedFormField } from "@rebasepro/app";
|
|
16
|
+
import { Alert, Button, cls, Dialog, DialogActions, DialogContent, DialogTitle, iconSize, paperMixin, Typography } from "@rebasepro/ui";
|
|
18
17
|
import { Formex, FormexController, useCreateFormex } from "@rebasepro/forms";
|
|
19
18
|
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
19
|
+
import { FieldBlock, isSelfLabellingProperty } from "./components/FieldBlock";
|
|
20
|
+
import { FormRail } from "./components/FormRail";
|
|
21
|
+
import { FormSections } from "./components/FormSections";
|
|
22
|
+
import { useRailVisible } from "./components/useRailVisible";
|
|
22
23
|
import { LabelWithIconAndTooltip } from "./components/LabelWithIconAndTooltip";
|
|
23
24
|
import { PropertyFieldBinding } from "./PropertyFieldBinding";
|
|
24
25
|
import { flattenKeys } from "@rebasepro/app";
|
|
@@ -28,9 +29,7 @@ import { EntityFormActions } from "./EntityFormActions";
|
|
|
28
29
|
import type { EntityFormActionsProps } from "../types/components/EntityFormActionsProps";
|
|
29
30
|
import { LocalChangesMenu } from "./components/LocalChangesMenu";
|
|
30
31
|
|
|
31
|
-
import {
|
|
32
|
-
import { getUserLabel, useResolvedUser } from "../hooks/useResolvedUsers";
|
|
33
|
-
import { getValueInPath, mergeDeep } from "@rebasepro/utils";
|
|
32
|
+
import { mergeDeep } from "@rebasepro/utils";
|
|
34
33
|
import {
|
|
35
34
|
getChanges,
|
|
36
35
|
zodToFormErrors
|
|
@@ -55,7 +54,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
55
54
|
entity,
|
|
56
55
|
initialDirtyValues,
|
|
57
56
|
onFormContextReady,
|
|
58
|
-
forceActionsAtTheBottom,
|
|
59
57
|
initialStatus,
|
|
60
58
|
className,
|
|
61
59
|
onStatusChange,
|
|
@@ -66,7 +64,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
66
64
|
Builder,
|
|
67
65
|
EntityFormActionsComponent = EntityFormActions,
|
|
68
66
|
showDefaultActions = true,
|
|
69
|
-
showEntityPath = true,
|
|
70
67
|
navigateBack: navigateBackProp,
|
|
71
68
|
children,
|
|
72
69
|
// Headless callback props
|
|
@@ -87,8 +84,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
87
84
|
onClearLocalChanges
|
|
88
85
|
}: EntityFormProps<M>) {
|
|
89
86
|
|
|
90
|
-
const customizationController = useCustomizationController();
|
|
91
|
-
|
|
92
87
|
const navigateBack = useCallback(() => {
|
|
93
88
|
if (navigateBackProp) {
|
|
94
89
|
navigateBackProp();
|
|
@@ -122,7 +117,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
122
117
|
}, [entityIdProp, status]);
|
|
123
118
|
|
|
124
119
|
const [entityId, setEntityId] = useState<string | number | undefined>(initialEntityId);
|
|
125
|
-
const [entityIdError, setEntityIdError] = useState<boolean>(false);
|
|
126
120
|
const [savingError, setSavingError] = useState<Error | undefined>();
|
|
127
121
|
const [isSavingAutoSave, setIsSavingAutoSave] = useState<boolean>(false);
|
|
128
122
|
const [discardDialogOpen, setDiscardDialogOpen] = useState<boolean>(false);
|
|
@@ -151,7 +145,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
151
145
|
const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
|
|
152
146
|
|
|
153
147
|
setSavingError(undefined);
|
|
154
|
-
setEntityIdError(false);
|
|
155
148
|
|
|
156
149
|
if (status === "existing") {
|
|
157
150
|
if (!entity?.id) throw Error("Form misconfiguration when saving, no id for existing entity");
|
|
@@ -307,26 +300,33 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
307
300
|
savingError,
|
|
308
301
|
status,
|
|
309
302
|
openEntityMode,
|
|
310
|
-
disabled
|
|
303
|
+
disabled,
|
|
304
|
+
isSaving: formex.isSubmitting || isSavingAutoSave
|
|
311
305
|
};
|
|
312
306
|
|
|
313
307
|
useEffect(() => {
|
|
314
308
|
onFormContextReady?.(formContext);
|
|
315
|
-
|
|
309
|
+
// `formex.version` only moves on `resetForm`, so these deps used to hand
|
|
310
|
+
// the container one snapshot at mount and never another. That was
|
|
311
|
+
// invisible while Save lived inside the form and read `formex` directly;
|
|
312
|
+
// once the identity bar owned Save, its `dirty` was frozen false and the
|
|
313
|
+
// button never enabled — for any field, not just the one that exposed
|
|
314
|
+
// it. Custom entity views read `formContext.values` through the same
|
|
315
|
+
// channel and were reading a stale copy too.
|
|
316
|
+
//
|
|
317
|
+
// Deps are the state *transitions* a container cares about, not
|
|
318
|
+
// `formex.values`: re-publishing on every keystroke would re-render the
|
|
319
|
+
// whole entity view per character.
|
|
320
|
+
}, [
|
|
321
|
+
formex.version, collection, entityId, path,
|
|
322
|
+
formex.dirty, formex.isSubmitting, formex.submitCount, isSavingAutoSave
|
|
323
|
+
]);
|
|
316
324
|
|
|
317
325
|
const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
|
|
318
326
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
//
|
|
322
|
-
const titleUser = useResolvedUser(isUserSelectProperty(collection, titlePropertyKey) && typeof rawTitle === "string"
|
|
323
|
-
? rawTitle
|
|
324
|
-
: undefined);
|
|
325
|
-
const title = titleUser
|
|
326
|
-
? getUserLabel(titleUser)
|
|
327
|
-
: (rawTitle !== undefined && rawTitle !== null
|
|
328
|
-
? resolveTitleToString(rawTitle)
|
|
329
|
-
: (collection.singularName ?? collection.name));
|
|
327
|
+
// The record's title is resolved by the container that renders the identity
|
|
328
|
+
// bar (see `useEntityDisplayTitle`), not here — the form no longer draws a
|
|
329
|
+
// heading of its own.
|
|
330
330
|
|
|
331
331
|
const modified = formex.dirty;
|
|
332
332
|
|
|
@@ -370,6 +370,145 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
370
370
|
|
|
371
371
|
const formFieldKeys = getFormFieldKeys(collection);
|
|
372
372
|
|
|
373
|
+
const layout = useMemo(() => resolveFormLayout({
|
|
374
|
+
collection,
|
|
375
|
+
fieldKeys: formFieldKeys,
|
|
376
|
+
status
|
|
377
|
+
}), [collection, formFieldKeys.join(","), status]);
|
|
378
|
+
|
|
379
|
+
const formElementRef = useRef<HTMLFormElement>(null);
|
|
380
|
+
// Measured, not a breakpoint: the rail holds fields the resolver moved
|
|
381
|
+
// out of the column, so when it cannot be shown they have to come back.
|
|
382
|
+
const railVisible = useRailVisible(formElementRef);
|
|
383
|
+
const showRail = !Builder && layout.hasRail && railVisible;
|
|
384
|
+
|
|
385
|
+
// Collapsed sections are per-section-key so a renamed title keeps its state,
|
|
386
|
+
// and initial state comes from the config rather than being forced open.
|
|
387
|
+
const [collapsedSections, setCollapsedSections] = useState<Set<string>>(
|
|
388
|
+
() => new Set(layout.sections.filter(s => s.collapsed).map(s => s.key))
|
|
389
|
+
);
|
|
390
|
+
const toggleSection = useCallback((key: string) => {
|
|
391
|
+
setCollapsedSections(prev => {
|
|
392
|
+
const next = new Set(prev);
|
|
393
|
+
if (next.has(key)) next.delete(key); else next.add(key);
|
|
394
|
+
return next;
|
|
395
|
+
});
|
|
396
|
+
}, []);
|
|
397
|
+
|
|
398
|
+
const errorKeys = useMemo(
|
|
399
|
+
() => new Set(formex.submitCount > 0 ? Object.keys(formex.errors) : []),
|
|
400
|
+
[formex.errors, formex.submitCount]
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
// Autofocus lands on the first field the user can actually type into. Tracked
|
|
404
|
+
// across the whole layout rather than per section, so a collapsed leading
|
|
405
|
+
// group does not steal it.
|
|
406
|
+
const autoFocusKey = useMemo(() => {
|
|
407
|
+
if (status !== "new" && status !== "copy") return undefined;
|
|
408
|
+
const ordered = [...layout.sections.flatMap(s => s.fields), ...layout.sidebar];
|
|
409
|
+
return ordered.find(field => {
|
|
410
|
+
const property = collection.properties?.[field.key];
|
|
411
|
+
return property ? !isFieldDisabled(property) : false;
|
|
412
|
+
})?.key;
|
|
413
|
+
}, [layout, collection, status, disabledProp]);
|
|
414
|
+
|
|
415
|
+
function isFieldDisabled(property: NonNullable<typeof collection.properties[string]>): boolean {
|
|
416
|
+
const isNew = status === "new" || status === "copy";
|
|
417
|
+
const isStringOrNumber = property.type === "string" || property.type === "number";
|
|
418
|
+
const isIdAndAuto = isStringOrNumber && "isId" in property && typeof property.isId === "string" && property.isId !== "manual";
|
|
419
|
+
return Boolean(
|
|
420
|
+
disabledProp
|
|
421
|
+
|| (!autoSave && formex.isSubmitting)
|
|
422
|
+
|| isReadOnly(property)
|
|
423
|
+
|| Boolean(property.admin?.disabled)
|
|
424
|
+
|| (!isNew && "isId" in property && Boolean(property.isId))
|
|
425
|
+
|| (isNew && isIdAndAuto)
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const renderField = (field: ResolvedFormField): React.ReactNode => {
|
|
430
|
+
|
|
431
|
+
if (field.additional) {
|
|
432
|
+
const additionalField = collection.additionalFields?.find(f => f.key === field.key);
|
|
433
|
+
if (!additionalField || !entity) return null;
|
|
434
|
+
|
|
435
|
+
const AdditionalFieldBuilder = additionalField.Builder;
|
|
436
|
+
if (!AdditionalFieldBuilder && !additionalField.value) {
|
|
437
|
+
throw new Error("When using additional fields you need to provide a Builder or a value");
|
|
438
|
+
}
|
|
439
|
+
const additionalFieldContext = formContext as unknown as AdditionalFieldDelegateProps["context"];
|
|
440
|
+
const child = AdditionalFieldBuilder
|
|
441
|
+
? <AdditionalFieldBuilder entity={entity} context={additionalFieldContext}/>
|
|
442
|
+
: <div className={"w-full"}>
|
|
443
|
+
{additionalField.value?.({
|
|
444
|
+
entity,
|
|
445
|
+
context: additionalFieldContext
|
|
446
|
+
})?.toString()}
|
|
447
|
+
</div>;
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<div key={`additional_${field.key}`}
|
|
451
|
+
className={spanClass(field.span)}>
|
|
452
|
+
<LabelWithIconAndTooltip
|
|
453
|
+
propertyKey={field.key}
|
|
454
|
+
icon={<AlignLeftIcon size={iconSize.small}/>}
|
|
455
|
+
title={additionalField.name}
|
|
456
|
+
className={"text-text-secondary dark:text-text-secondary-dark"}/>
|
|
457
|
+
<div className={cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-auto no-scrollbar")}>
|
|
458
|
+
<ErrorBoundary>
|
|
459
|
+
{child}
|
|
460
|
+
</ErrorBoundary>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const property = collection.properties?.[field.key];
|
|
467
|
+
if (!property) {
|
|
468
|
+
console.warn(`Property ${field.key} not found in collection ${collection.name} in properties or additional fields. Skipping.`);
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const underlyingValueHasChanged: boolean =
|
|
473
|
+
!!underlyingChanges &&
|
|
474
|
+
Object.keys(underlyingChanges).includes(field.key) &&
|
|
475
|
+
formex.touched[field.key];
|
|
476
|
+
|
|
477
|
+
const disabled = isFieldDisabled(property);
|
|
478
|
+
// The panel-style editors carry their own header; everything else is
|
|
479
|
+
// labelled by FieldBlock, which is what puts every label in one place.
|
|
480
|
+
const selfLabelling = isSelfLabellingProperty(property);
|
|
481
|
+
|
|
482
|
+
const cmsFormFieldProps: PropertyFieldBindingProps<M> = {
|
|
483
|
+
propertyKey: field.key,
|
|
484
|
+
disabled,
|
|
485
|
+
property,
|
|
486
|
+
// FieldBlock owns the description now, so the binding must not
|
|
487
|
+
// repeat it — it still renders validation errors, which belong
|
|
488
|
+
// against the control rather than under the label.
|
|
489
|
+
includeDescription: false,
|
|
490
|
+
hideLabel: !selfLabelling,
|
|
491
|
+
underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
|
|
492
|
+
context: formContext,
|
|
493
|
+
partOfArray: false,
|
|
494
|
+
minimalistView: false,
|
|
495
|
+
// One height for every control in the form. Left to their own
|
|
496
|
+
// defaults, a text field came out 48px, a select 44 and a switch 64.
|
|
497
|
+
size: "large",
|
|
498
|
+
autoFocus: autoFocusKey === field.key
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
return (
|
|
502
|
+
<div key={`field_${field.key}`} className={spanClass(field.span)}>
|
|
503
|
+
<FieldBlock propertyKey={field.key}
|
|
504
|
+
property={property}
|
|
505
|
+
showLabel={!selfLabelling}>
|
|
506
|
+
<PropertyFieldBinding {...cmsFormFieldProps}/>
|
|
507
|
+
</FieldBlock>
|
|
508
|
+
</div>
|
|
509
|
+
);
|
|
510
|
+
};
|
|
511
|
+
|
|
373
512
|
const formFields = () => {
|
|
374
513
|
|
|
375
514
|
if (Builder) {
|
|
@@ -380,89 +519,30 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
380
519
|
formContext={formContext}
|
|
381
520
|
/>;
|
|
382
521
|
}
|
|
383
|
-
const isNewEntity = status === "new" || status === "copy";
|
|
384
|
-
let firstFocusableIndex = -1;
|
|
385
|
-
|
|
386
|
-
return (
|
|
387
|
-
<FormLayout>
|
|
388
|
-
{formFieldKeys.map((key) => {
|
|
389
|
-
const property = collection.properties?.[key];
|
|
390
|
-
if (property) {
|
|
391
|
-
|
|
392
|
-
const underlyingValueHasChanged: boolean =
|
|
393
|
-
!!underlyingChanges &&
|
|
394
|
-
Object.keys(underlyingChanges).includes(key) &&
|
|
395
|
-
formex.touched[key];
|
|
396
|
-
const isNew = status === "new" || status === "copy";
|
|
397
|
-
const isStringOrNumber = property.type === "string" || property.type === "number";
|
|
398
|
-
const isIdAndAuto = isStringOrNumber && "isId" in property && typeof property.isId === "string" && property.isId !== "manual";
|
|
399
|
-
const disabled = disabledProp || (!autoSave && formex.isSubmitting) || isReadOnly(property) || Boolean(property.admin?.disabled) || (!isNew && "isId" in property && Boolean(property.isId)) || (isNew && isIdAndAuto);
|
|
400
|
-
const hidden = isHidden(property);
|
|
401
|
-
if (hidden) return null;
|
|
402
|
-
const widthPercentage = property.admin?.widthPercentage ?? 100;
|
|
403
|
-
|
|
404
|
-
const shouldAutoFocus = isNewEntity && !disabled && firstFocusableIndex === -1;
|
|
405
|
-
if (shouldAutoFocus) firstFocusableIndex = 0;
|
|
406
|
-
|
|
407
|
-
const cmsFormFieldProps: PropertyFieldBindingProps<M> = {
|
|
408
|
-
propertyKey: key,
|
|
409
|
-
disabled,
|
|
410
|
-
property,
|
|
411
|
-
includeDescription: Boolean(property.description),
|
|
412
|
-
underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
|
|
413
|
-
context: formContext,
|
|
414
|
-
partOfArray: false,
|
|
415
|
-
minimalistView: false,
|
|
416
|
-
autoFocus: shouldAutoFocus
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
return (
|
|
420
|
-
<FormEntry propertyKey={key}
|
|
421
|
-
widthPercentage={widthPercentage}
|
|
422
|
-
key={`field_${key}`}>
|
|
423
|
-
<PropertyFieldBinding {...cmsFormFieldProps}/>
|
|
424
|
-
</FormEntry>
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
522
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
<div
|
|
452
|
-
className={cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-scroll no-scrollbar")}>
|
|
453
|
-
<ErrorBoundary>
|
|
454
|
-
{child}
|
|
455
|
-
</ErrorBoundary>
|
|
456
|
-
</div>
|
|
457
|
-
</div>
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
console.warn(`Property ${key} not found in collection ${collection.name} in properties or additional fields. Skipping.`);
|
|
462
|
-
return null;
|
|
463
|
-
}).filter(Boolean)}
|
|
464
|
-
</FormLayout>
|
|
465
|
-
);
|
|
523
|
+
const sections = showRail || !layout.sidebar.length
|
|
524
|
+
? layout.sections
|
|
525
|
+
: [
|
|
526
|
+
...layout.sections,
|
|
527
|
+
// Trailing, not leading. Beside the column these read as
|
|
528
|
+
// peripheral; stacked above it they pushed the record's own name
|
|
529
|
+
// below its status, which is the wrong thing to meet first.
|
|
530
|
+
// Settings come after the content they apply to.
|
|
531
|
+
{
|
|
532
|
+
key: "__rail",
|
|
533
|
+
title: "Settings",
|
|
534
|
+
collapsible: false,
|
|
535
|
+
collapsed: false,
|
|
536
|
+
fields: layout.sidebar
|
|
537
|
+
}
|
|
538
|
+
];
|
|
539
|
+
|
|
540
|
+
return <FormSections
|
|
541
|
+
sections={sections}
|
|
542
|
+
collapsed={collapsedSections}
|
|
543
|
+
onToggle={toggleSection}
|
|
544
|
+
errorKeys={errorKeys}
|
|
545
|
+
renderField={renderField}/>;
|
|
466
546
|
};
|
|
467
547
|
|
|
468
548
|
const formRef = useRef<HTMLDivElement>(null);
|
|
@@ -473,25 +553,15 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
473
553
|
<>
|
|
474
554
|
{beforeFields}
|
|
475
555
|
|
|
476
|
-
{
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
<Alert color={"warning"} size={"small"} outerClassName={"w-full mb-4 text-xs"}>
|
|
485
|
-
This entity does not exist yet
|
|
486
|
-
</Alert>}
|
|
487
|
-
|
|
488
|
-
{showEntityPath && <Alert color={"base"} outerClassName={"w-full"} size={"small"}>
|
|
489
|
-
<code
|
|
490
|
-
className={"text-xs select-all text-text-secondary dark:text-text-secondary-dark"}>
|
|
491
|
-
{entity?.path ?? path}/{entityId}
|
|
492
|
-
</code>
|
|
556
|
+
{/* The title, the `path/id` chip and the 72px gap under them used to
|
|
557
|
+
live here — 219px above the first field, carrying nothing, and
|
|
558
|
+
scrolling the record's identity away on the first wheel click.
|
|
559
|
+
It is now in the persistent bar, where it stays put. */}
|
|
560
|
+
|
|
561
|
+
{!entity?.values && initialStatus === "existing" &&
|
|
562
|
+
<Alert color={"warning"} size={"small"} outerClassName={"w-full mb-4 text-xs"}>
|
|
563
|
+
This entity does not exist yet
|
|
493
564
|
</Alert>}
|
|
494
|
-
</div>}
|
|
495
565
|
|
|
496
566
|
{children}
|
|
497
567
|
|
|
@@ -504,7 +574,7 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
504
574
|
</Alert>}
|
|
505
575
|
|
|
506
576
|
{formContext && <>
|
|
507
|
-
<div
|
|
577
|
+
<div ref={formRef}>
|
|
508
578
|
{formFields()}
|
|
509
579
|
<ErrorFocus containerRef={formRef}/>
|
|
510
580
|
</div>
|
|
@@ -512,8 +582,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
512
582
|
|
|
513
583
|
{afterFields}
|
|
514
584
|
|
|
515
|
-
{forceActionsAtTheBottom && <div className="h-16"/>}
|
|
516
|
-
|
|
517
585
|
<Dialog open={discardDialogOpen} onOpenChange={setDiscardDialogOpen} maxWidth={"sm"}>
|
|
518
586
|
<DialogTitle>{status === "existing" ? "Discard changes?" : "Clear form?"}</DialogTitle>
|
|
519
587
|
<DialogContent>
|
|
@@ -553,7 +621,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
553
621
|
collection={collection}
|
|
554
622
|
path={path}
|
|
555
623
|
entity={entity}
|
|
556
|
-
layout={forceActionsAtTheBottom ? "bottom" : "responsive"}
|
|
557
624
|
savingError={savingError}
|
|
558
625
|
formex={formex as FormexController<Record<string, unknown>>}
|
|
559
626
|
disabled={actionsDisabled}
|
|
@@ -568,6 +635,7 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
568
635
|
return (
|
|
569
636
|
<Formex value={formex}>
|
|
570
637
|
<form
|
|
638
|
+
ref={formElementRef}
|
|
571
639
|
onSubmit={formex.handleSubmit}
|
|
572
640
|
onReset={(e) => {
|
|
573
641
|
e.preventDefault();
|
|
@@ -578,55 +646,56 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
578
646
|
}
|
|
579
647
|
}}
|
|
580
648
|
noValidate
|
|
581
|
-
className={cls("@container flex-1 flex flex-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
649
|
+
className={cls("@container/form flex-1 flex flex-col w-full min-h-0", className)}>
|
|
650
|
+
|
|
651
|
+
{/* The actions bar is a sibling of this row rather than absolutely
|
|
652
|
+
positioned over it, so the scroll area ends where the bar
|
|
653
|
+
begins. As an overlay it hid the last field, and the bottom
|
|
654
|
+
padding that compensated was a guess at its height. */}
|
|
655
|
+
<div className={"flex-1 min-h-0 flex flex-row w-full"}>
|
|
656
|
+
|
|
657
|
+
{/* Main column. Its own container scope, so a field's span
|
|
658
|
+
answers to the column it sits in rather than to the window —
|
|
659
|
+
the same form renders at four very different widths. */}
|
|
660
|
+
{/* `items-start`: the column is a flex item, so without it the cross-axis
|
|
661
|
+
default stretches it to the scroller's height. Its content then
|
|
662
|
+
overflows its own box and the bottom padding sits at the bottom of
|
|
663
|
+
the *viewport* instead of after the last field — which is why the
|
|
664
|
+
form appeared to have no padding however large `pb-*` grew. */}
|
|
665
|
+
<div className={"flex-1 min-w-0 overflow-y-auto flex justify-center items-start"}>
|
|
592
666
|
<div
|
|
593
|
-
|
|
667
|
+
id={`form_${path}`}
|
|
668
|
+
className={cls(
|
|
669
|
+
"@container/col w-full max-w-3xl 2xl:max-w-4xl flex flex-col",
|
|
670
|
+
openEntityMode === "dialog"
|
|
671
|
+
? "pt-5 pb-8 px-6 sm:px-8"
|
|
672
|
+
: "pt-6 pb-12 px-5 sm:px-8"
|
|
673
|
+
)}>
|
|
594
674
|
|
|
595
675
|
{manualApplyLocalChanges && hasLocalChanges && localChangesCacheKey &&
|
|
596
|
-
<
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
<LoaderIcon size={iconSize.smallest} className={"animate-spin"}/>
|
|
611
|
-
</Chip>
|
|
612
|
-
</Tooltip>
|
|
613
|
-
: formex.dirty
|
|
614
|
-
? <Tooltip title={"Form has been modified"}>
|
|
615
|
-
<Chip size={"small"} className={"py-1"} colorScheme={"orangeDarker"}>
|
|
616
|
-
<PencilIcon size={iconSize.smallest}/>
|
|
617
|
-
</Chip>
|
|
618
|
-
</Tooltip>
|
|
619
|
-
: <Tooltip title={"Form is in sync"}>
|
|
620
|
-
<Chip size={"small"} className={"py-1"}>
|
|
621
|
-
<CheckIcon size={iconSize.smallest}/>
|
|
622
|
-
</Chip>
|
|
623
|
-
</Tooltip>}
|
|
624
|
-
</div>
|
|
625
|
-
|
|
626
|
-
{formView}
|
|
676
|
+
<div className={"flex justify-end mb-3"}>
|
|
677
|
+
<LocalChangesMenu<M>
|
|
678
|
+
cacheKey={localChangesCacheKey}
|
|
679
|
+
properties={collection.properties}
|
|
680
|
+
cachedData={localChangesData as Partial<M>}
|
|
681
|
+
formex={formex}
|
|
682
|
+
onClearLocalChanges={() => {
|
|
683
|
+
setLocalChangesCleared(true);
|
|
684
|
+
onClearLocalChanges?.();
|
|
685
|
+
}}
|
|
686
|
+
/>
|
|
687
|
+
</div>}
|
|
688
|
+
|
|
689
|
+
{formView}
|
|
627
690
|
|
|
691
|
+
</div>
|
|
628
692
|
</div>
|
|
629
693
|
|
|
694
|
+
{showRail && <FormRail
|
|
695
|
+
fields={layout.sidebar}
|
|
696
|
+
showRecordMeta={layout.showRecordMeta && status === "existing"}
|
|
697
|
+
entity={entity as Entity<Record<string, unknown>> | undefined}
|
|
698
|
+
renderField={renderField}/>}
|
|
630
699
|
</div>
|
|
631
700
|
|
|
632
701
|
{dialogActions}
|
|
@@ -637,6 +706,25 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
637
706
|
);
|
|
638
707
|
}
|
|
639
708
|
|
|
709
|
+
/**
|
|
710
|
+
* Grid placement for a span.
|
|
711
|
+
*
|
|
712
|
+
* Written out as literal class strings rather than composed, because Tailwind
|
|
713
|
+
* scans source text: a template literal would produce classes that exist at
|
|
714
|
+
* runtime and were never generated into the stylesheet.
|
|
715
|
+
*
|
|
716
|
+
* Below the column breakpoint every field takes the single available column, so
|
|
717
|
+
* the side panel, the dialog and mobile all get a plain stack.
|
|
718
|
+
*/
|
|
719
|
+
function spanClass(span: 1 | 2 | 3 | 4): string {
|
|
720
|
+
switch (span) {
|
|
721
|
+
case 1: return "@2xl/col:col-span-1";
|
|
722
|
+
case 2: return "@2xl/col:col-span-2";
|
|
723
|
+
case 3: return "@2xl/col:col-span-3";
|
|
724
|
+
case 4: return "@2xl/col:col-span-4";
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
640
728
|
function useOnAutoSave<M extends Record<string, unknown>>(autoSave: undefined | boolean, formex: FormexController<M>, lastSavedValues: React.MutableRefObject<EntityValues<M> | undefined>, save: (values: EntityValues<M>) => Promise<Entity<M> | void>) {
|
|
641
729
|
useEffect(() => {
|
|
642
730
|
if (!autoSave) return;
|