@firecms/core 3.0.0-canary.283 → 3.0.0-canary.285

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.
@@ -44,9 +44,9 @@ export interface EntityEditViewProps<M extends Record<string, any>> {
44
44
  * an entity is opened.
45
45
  */
46
46
  export declare function EntityEditView<M extends Record<string, any>, USER extends User>({ entityId, ...props }: EntityEditViewProps<M>): import("react/jsx-runtime").JSX.Element;
47
- export declare function EntityEditViewInner<M extends Record<string, any>>({ path, fullIdPath, entityId, selectedTab: selectedTabProp, collection, parentCollectionIds, onValuesModified, onSaved, onTabChange, entity, cachedDirtyValues, dataLoading, layout, barActions, status, setStatus, formProps, canEdit }: EntityEditViewProps<M> & {
47
+ export declare function EntityEditViewInner<M extends Record<string, any>>({ path, fullIdPath, entityId, selectedTab: selectedTabProp, collection, parentCollectionIds, onValuesModified, onSaved, onTabChange, entity, initialDirtyValues, dataLoading, layout, barActions, status, setStatus, formProps, canEdit }: EntityEditViewProps<M> & {
48
48
  entity?: Entity<M>;
49
- cachedDirtyValues?: Partial<M>;
49
+ initialDirtyValues?: Partial<M>;
50
50
  dataLoading: boolean;
51
51
  status: EntityStatus;
52
52
  setStatus: (status: EntityStatus) => void;
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { Entity, EntityCollection, EntityCustomViewParams, EntityStatus, FormContext } from "../types";
2
+ import { AuthController, Entity, EntityCollection, EntityCustomViewParams, EntityStatus, EntityValues, FormContext, PropertyConfig } from "../types";
3
3
  import { FormexController } from "@firecms/formex";
4
4
  import { ValidationError } from "yup";
5
5
  import { EntityFormActionsProps } from "./EntityFormActions";
@@ -46,5 +46,7 @@ export type EntityFormProps<M extends Record<string, any>> = {
46
46
  Builder?: React.ComponentType<EntityCustomViewParams<M>>;
47
47
  children?: React.ReactNode;
48
48
  };
49
+ export declare function extractTouchedValues(values: any, touched: Record<string, boolean>): Record<string, any>;
49
50
  export declare function EntityForm<M extends Record<string, any>>({ path, fullIdPath, entityId: entityIdProp, collection, onValuesModified, onIdChange, onSaved, entity, initialDirtyValues, onFormContextReady, forceActionsAtTheBottom, initialStatus, className, onStatusChange, onEntityChange, openEntityMode, formex: formexProp, disabled: disabledProp, Builder, EntityFormActionsComponent, showDefaultActions, showEntityPath, children }: EntityFormProps<M>): import("react/jsx-runtime").JSX.Element;
51
+ export declare function getInitialEntityValues<M extends object>(authController: AuthController, collection: EntityCollection, path: string, status: "new" | "existing" | "copy", entity: Entity<M> | undefined, propertyConfigs?: Record<string, PropertyConfig>): Partial<EntityValues<M>>;
50
52
  export declare function yupToFormErrors(yupError: ValidationError): Record<string, any>;
@@ -1,4 +1,5 @@
1
- export * from "./EntityForm";
1
+ export { EntityForm, yupToFormErrors, } from "./EntityForm";
2
+ export type { EntityFormProps } from "./EntityForm";
2
3
  export { SelectFieldBinding } from "./field_bindings/SelectFieldBinding";
3
4
  export { MultiSelectFieldBinding } from "./field_bindings/MultiSelectFieldBinding";
4
5
  export { ArrayOfReferencesFieldBinding } from "./field_bindings/ArrayOfReferencesFieldBinding";
package/dist/index.es.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { jsx, Fragment, jsxs } from "react/jsx-runtime";
2
2
  import { c } from "react-compiler-runtime";
3
3
  import * as React from "react";
4
- import React__default, { useRef, useEffect, useContext, useCallback, useMemo, useState, createElement, createRef, createContext, forwardRef, useLayoutEffect, useDeferredValue } from "react";
4
+ import React__default, { useRef, useEffect, useContext, useCallback, useMemo, useState, createElement, createRef, createContext, forwardRef, useLayoutEffect } from "react";
5
5
  import { getColorSchemeForSeed, CHIP_COLORS, FunctionsIcon, CircleIcon, iconKeys, coolIconKeys, Icon, Tooltip, ErrorIcon, Typography, IconButton, ContentCopyIcon, OpenInNewIcon, DescriptionIcon, cls, Skeleton, Chip, defaultBorderMixin, KeyboardTabIcon, Checkbox, AccountCircleIcon, Markdown, TextareaAutosize, focusedDisabled, MultiSelect, MultiSelectItem, Select, SelectItem, BooleanSwitch, DateTimeField, paperMixin, EditIcon, DoNotDisturbOnIcon, Menu, MenuItem, MoreVertIcon, CircularProgress, SearchBar, Badge, ArrowUpwardIcon, Popover, FilterListIcon, Button, CenteredView, AssignmentIcon, Label, CloseIcon, TextField, BooleanSwitchWithLabel, useOutsideAlerter, Dialog, DialogTitle, DialogContent, DialogActions, FileCopyIcon, DeleteIcon, AddIcon, StarIcon, Collapse, ExpandablePanel, ArrowForwardIcon, Card, cardMixin, cardClickableMixin, Container, LoadingButton, Alert, CheckIcon, NotesIcon, InfoIcon, fieldBackgroundMixin, RemoveIcon, fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, ArrowDropDownIcon, FilterListOffIcon, SearchIcon, Avatar, DarkModeIcon, LightModeIcon, BrightnessMediumIcon, LogoutIcon, HandleIcon, KeyboardArrowUpIcon, KeyboardArrowDownIcon, debounce, Sheet, Tab, Tabs, CodeIcon, OpenInFullIcon, ViewStreamIcon, RepeatIcon, BallotIcon, ScheduleIcon, AddLinkIcon, LinkIcon, DriveFolderUploadIcon, UploadFileIcon, FormatListNumberedIcon, NumbersIcon, PersonIcon, ListAltIcon, ListIcon, FlagIcon, MailIcon, HttpIcon, FormatQuoteIcon, SubjectIcon, ShortTextIcon, MenuIcon, ChevronLeftIcon } from "@firecms/ui";
6
6
  import { SnackbarProvider as SnackbarProvider$1, useSnackbar } from "notistack";
7
7
  import hash from "object-hash";
8
- import { getIn, useFormex, setIn, useCreateFormex, Formex, Field } from "@firecms/formex";
8
+ import { getIn, useFormex, setIn, useCreateFormex, flattenKeys, Formex, Field } from "@firecms/formex";
9
9
  import { useNavigate, useLocation, Link, NavLink, Routes, Route, createBrowserRouter, RouterProvider } from "react-router-dom";
10
10
  import Fuse from "fuse.js";
11
11
  import equal from "react-fast-compare";
@@ -9756,27 +9756,6 @@ function customReviver(key, value) {
9756
9756
  }
9757
9757
  return value;
9758
9758
  }
9759
- if (isLocalStorageAvailable) {
9760
- try {
9761
- for (let i = 0; i < localStorage.length; i++) {
9762
- const fullKey = localStorage.key(i);
9763
- if (fullKey && fullKey.startsWith(LOCAL_STORAGE_PREFIX)) {
9764
- const path = fullKey.substring(LOCAL_STORAGE_PREFIX.length);
9765
- const entityString = localStorage.getItem(fullKey);
9766
- if (entityString) {
9767
- try {
9768
- const entity = JSON.parse(entityString, customReviver);
9769
- entityCache.set(path, entity);
9770
- } catch (parseError) {
9771
- console.error(`Failed to parse entity for path "${path}" from localStorage:`, parseError);
9772
- }
9773
- }
9774
- }
9775
- }
9776
- } catch (error) {
9777
- console.error("Error accessing localStorage during initialization:", error);
9778
- }
9779
- }
9780
9759
  function saveEntityToCache(path, data) {
9781
9760
  entityCache.set(path, data);
9782
9761
  if (isLocalStorageAvailable) {
@@ -9789,11 +9768,11 @@ function saveEntityToCache(path, data) {
9789
9768
  }
9790
9769
  }
9791
9770
  }
9792
- function getEntityFromCache(path) {
9771
+ function getEntityFromCache(path, useLocalStorage = true) {
9793
9772
  if (entityCache.has(path)) {
9794
9773
  return entityCache.get(path);
9795
9774
  }
9796
- if (isLocalStorageAvailable) {
9775
+ if (isLocalStorageAvailable && useLocalStorage) {
9797
9776
  try {
9798
9777
  const key = LOCAL_STORAGE_PREFIX + path;
9799
9778
  const entityString = localStorage.getItem(key);
@@ -14989,61 +14968,60 @@ function CustomIdField({
14989
14968
  ] });
14990
14969
  }
14991
14970
  const ErrorFocus = (t0) => {
14992
- const $ = c(6);
14971
+ const $ = c(10);
14993
14972
  const {
14994
14973
  containerRef
14995
14974
  } = t0;
14996
14975
  const {
14997
- isSubmitting,
14998
14976
  isValidating,
14999
- errors
14977
+ errors,
14978
+ version
15000
14979
  } = useFormex();
14980
+ const prevVersion = useRef(version);
15001
14981
  let t1;
15002
- let t2;
15003
- if ($[0] !== containerRef || $[1] !== errors || $[2] !== isSubmitting || $[3] !== isValidating) {
14982
+ if ($[0] !== containerRef?.current || $[1] !== errors || $[2] !== isValidating || $[3] !== version) {
15004
14983
  t1 = () => {
14984
+ if (version === prevVersion.current) {
14985
+ return;
14986
+ }
15005
14987
  const keys = Object.keys(errors);
15006
- if (keys.length > 0 && isSubmitting && !isValidating) {
14988
+ if (!isValidating && keys.length > 0) {
15007
14989
  const errorElement = containerRef?.current?.querySelector(`#form_field_${keys[0]}`);
15008
- if (errorElement && containerRef?.current) {
15009
- const scrollableParent = getScrollableParent(containerRef.current);
15010
- if (scrollableParent) {
15011
- const top = errorElement.getBoundingClientRect().top;
15012
- scrollableParent.scrollTo({
15013
- top: scrollableParent.scrollTop + top - 196,
15014
- behavior: "smooth"
15015
- });
15016
- }
14990
+ if (errorElement) {
14991
+ errorElement.scrollIntoView({
14992
+ behavior: "smooth",
14993
+ block: "center"
14994
+ });
15017
14995
  const input = errorElement.querySelector("input");
15018
14996
  if (input) {
15019
14997
  input.focus();
15020
14998
  }
15021
14999
  }
15000
+ prevVersion.current = version;
15022
15001
  }
15023
15002
  };
15024
- t2 = [isSubmitting, isValidating, errors, containerRef];
15025
- $[0] = containerRef;
15003
+ $[0] = containerRef?.current;
15026
15004
  $[1] = errors;
15027
- $[2] = isSubmitting;
15028
- $[3] = isValidating;
15005
+ $[2] = isValidating;
15006
+ $[3] = version;
15029
15007
  $[4] = t1;
15030
- $[5] = t2;
15031
15008
  } else {
15032
15009
  t1 = $[4];
15033
- t2 = $[5];
15010
+ }
15011
+ let t2;
15012
+ if ($[5] !== containerRef || $[6] !== errors || $[7] !== isValidating || $[8] !== version) {
15013
+ t2 = [isValidating, errors, containerRef, version];
15014
+ $[5] = containerRef;
15015
+ $[6] = errors;
15016
+ $[7] = isValidating;
15017
+ $[8] = version;
15018
+ $[9] = t2;
15019
+ } else {
15020
+ t2 = $[9];
15034
15021
  }
15035
15022
  useEffect(t1, t2);
15036
15023
  return null;
15037
15024
  };
15038
- const isScrollable = (ele) => {
15039
- const hasScrollableContent = ele && ele.scrollHeight > ele.clientHeight;
15040
- const overflowYStyle = ele ? window.getComputedStyle(ele).overflowY : null;
15041
- const isOverflowHidden = overflowYStyle && overflowYStyle.indexOf("hidden") !== -1;
15042
- return hasScrollableContent && !isOverflowHidden;
15043
- };
15044
- const getScrollableParent = (ele) => {
15045
- return !ele || ele === document.body ? document.body : isScrollable(ele) ? ele : getScrollableParent(ele.parentNode);
15046
- };
15047
15025
  function EntityFormActions(t0) {
15048
15026
  const $ = c(16);
15049
15027
  const {
@@ -15064,7 +15042,7 @@ function EntityFormActions(t0) {
15064
15042
  const context = useFireCMSContext();
15065
15043
  const sideEntityController = useSideEntityController();
15066
15044
  let t1;
15067
- if ($[0] !== collection || $[1] !== context || $[2] !== disabled || $[3] !== entity || $[4] !== formContext || $[5] !== formex.isSubmitting || $[6] !== fullIdPath || $[7] !== fullPath || $[8] !== layout || $[9] !== navigateBack || $[10] !== openEntityMode || $[11] !== pluginActions || $[12] !== savingError || $[13] !== sideEntityController || $[14] !== status) {
15045
+ if ($[0] !== collection || $[1] !== context || $[2] !== disabled || $[3] !== entity || $[4] !== formContext || $[5] !== formex || $[6] !== fullIdPath || $[7] !== fullPath || $[8] !== layout || $[9] !== navigateBack || $[10] !== openEntityMode || $[11] !== pluginActions || $[12] !== savingError || $[13] !== sideEntityController || $[14] !== status) {
15068
15046
  t1 = layout === "bottom" ? buildBottomActions$1({
15069
15047
  fullPath,
15070
15048
  fullIdPath,
@@ -15073,13 +15051,13 @@ function EntityFormActions(t0) {
15073
15051
  collection,
15074
15052
  context,
15075
15053
  sideEntityController,
15076
- isSubmitting: formex.isSubmitting,
15077
15054
  disabled,
15078
15055
  status,
15079
15056
  pluginActions,
15080
15057
  openEntityMode,
15081
15058
  navigateBack,
15082
- formContext
15059
+ formContext,
15060
+ formex
15083
15061
  }) : buildSideActions$1({
15084
15062
  fullPath,
15085
15063
  fullIdPath,
@@ -15088,18 +15066,18 @@ function EntityFormActions(t0) {
15088
15066
  collection,
15089
15067
  context,
15090
15068
  sideEntityController,
15091
- isSubmitting: formex.isSubmitting,
15092
15069
  disabled,
15093
15070
  status,
15094
15071
  pluginActions,
15095
- openEntityMode
15072
+ openEntityMode,
15073
+ formex
15096
15074
  });
15097
15075
  $[0] = collection;
15098
15076
  $[1] = context;
15099
15077
  $[2] = disabled;
15100
15078
  $[3] = entity;
15101
15079
  $[4] = formContext;
15102
- $[5] = formex.isSubmitting;
15080
+ $[5] = formex;
15103
15081
  $[6] = fullIdPath;
15104
15082
  $[7] = fullPath;
15105
15083
  $[8] = layout;
@@ -15124,14 +15102,15 @@ function buildBottomActions$1({
15124
15102
  collection,
15125
15103
  context,
15126
15104
  sideEntityController,
15127
- isSubmitting,
15128
15105
  disabled,
15129
15106
  status,
15130
15107
  pluginActions,
15131
15108
  openEntityMode,
15132
15109
  navigateBack,
15133
- formContext
15110
+ formContext,
15111
+ formex
15134
15112
  }) {
15113
+ const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
15135
15114
  return /* @__PURE__ */ jsxs(DialogActions, { position: "absolute", children: [
15136
15115
  savingError && /* @__PURE__ */ jsx("div", { className: "text-right", children: /* @__PURE__ */ jsx(Typography, { color: "error", children: savingError.message }) }),
15137
15116
  entity && (formActions ?? []).length > 0 && /* @__PURE__ */ jsx("div", { className: "flex-grow flex overflow-auto no-scrollbar", children: (formActions ?? []).map((action) => /* @__PURE__ */ jsx(IconButton, { color: "primary", onClick: (event) => {
@@ -15150,8 +15129,8 @@ function buildBottomActions$1({
15150
15129
  });
15151
15130
  }, children: action.icon }, action.name)) }),
15152
15131
  pluginActions,
15153
- /* @__PURE__ */ jsx(Button, { variant: "text", disabled: disabled || isSubmitting, color: "primary", type: "reset", children: status === "existing" ? "Discard" : "Clear" }),
15154
- /* @__PURE__ */ jsxs(Button, { variant: "filled", color: "primary", type: "submit", disabled: disabled || isSubmitting, children: [
15132
+ /* @__PURE__ */ jsx(Button, { variant: "text", disabled: disabled || formex.isSubmitting, color: "primary", type: "reset", children: status === "existing" ? "Discard" : "Clear" }),
15133
+ /* @__PURE__ */ jsxs(Button, { variant: "filled", color: "primary", type: "submit", disabled: disabled || formex.isSubmitting, startIcon: hasErrors ? /* @__PURE__ */ jsx(ErrorIcon, {}) : void 0, children: [
15155
15134
  status === "existing" && "Save",
15156
15135
  status === "copy" && "Create copy",
15157
15136
  status === "new" && "Create"
@@ -15168,22 +15147,35 @@ function buildSideActions$1({
15168
15147
  collection,
15169
15148
  context,
15170
15149
  sideEntityController,
15171
- isSubmitting,
15172
15150
  disabled,
15173
15151
  status,
15174
- pluginActions
15152
+ pluginActions,
15153
+ formex
15175
15154
  }) {
15155
+ const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
15176
15156
  return /* @__PURE__ */ jsxs("div", { className: cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin), children: [
15177
- /* @__PURE__ */ jsxs(LoadingButton, { fullWidth: true, variant: "filled", color: "primary", type: "submit", size: "large", disabled: disabled || isSubmitting, children: [
15157
+ /* @__PURE__ */ jsxs(LoadingButton, { fullWidth: true, variant: "filled", color: "primary", type: "submit", size: "large", startIcon: hasErrors ? /* @__PURE__ */ jsx(ErrorIcon, {}) : void 0, disabled: disabled || formex.isSubmitting, children: [
15178
15158
  status === "existing" && "Save",
15179
15159
  status === "copy" && "Create copy",
15180
15160
  status === "new" && "Create"
15181
15161
  ] }),
15182
- /* @__PURE__ */ jsx(Button, { fullWidth: true, variant: "text", disabled: disabled || isSubmitting, type: "reset", children: status === "existing" ? "Discard" : "Clear" }),
15162
+ /* @__PURE__ */ jsx(Button, { fullWidth: true, variant: "text", disabled: disabled || formex.isSubmitting, type: "reset", children: status === "existing" ? "Discard" : "Clear" }),
15183
15163
  pluginActions,
15184
15164
  savingError && /* @__PURE__ */ jsx("div", { className: "text-right", children: /* @__PURE__ */ jsx(Typography, { color: "error", children: savingError.message }) })
15185
15165
  ] });
15186
15166
  }
15167
+ function extractTouchedValues(values, touched) {
15168
+ let acc = {};
15169
+ if (!touched || typeof touched !== "object") {
15170
+ return acc;
15171
+ }
15172
+ Object.entries(touched).forEach(([key, value]) => {
15173
+ if (value) {
15174
+ acc = setIn(acc, key, getIn(values, key));
15175
+ }
15176
+ });
15177
+ return acc;
15178
+ }
15187
15179
  function EntityForm({
15188
15180
  path,
15189
15181
  fullIdPath,
@@ -15241,7 +15233,7 @@ function EntityForm({
15241
15233
  const customizationController = useCustomizationController();
15242
15234
  const context = useFireCMSContext();
15243
15235
  const analyticsController = useAnalyticsController();
15244
- const [underlyingChanges, setUnderlyingChanges] = useState({});
15236
+ const [underlyingChanges] = useState({});
15245
15237
  const [customIdLoading, setCustomIdLoading] = useState(false);
15246
15238
  const mustSetCustomId = (status === "new" || status === "copy") && Boolean(collection.customId) && collection.customId !== "optional";
15247
15239
  const initialEntityId = useMemo(() => {
@@ -15289,16 +15281,30 @@ function EntityForm({
15289
15281
  formexController.setSubmitting(false);
15290
15282
  });
15291
15283
  };
15284
+ const baseInitialValues = getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs);
15285
+ const initialValues = initialDirtyValues ? mergeDeep(baseInitialValues, initialDirtyValues) : baseInitialValues;
15286
+ const initialDirty = Boolean(initialDirtyValues) && initialDirtyValues && Object.keys(initialDirtyValues).length > 0;
15292
15287
  const formex = formexProp ?? useCreateFormex({
15293
- initialValues: initialDirtyValues ?? getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs),
15294
- initialDirty: Boolean(initialDirtyValues),
15288
+ initialValues,
15289
+ initialDirty,
15290
+ initialTouched: initialDirtyValues ? flattenKeys(initialDirtyValues).reduce((previousValue, currentValue) => ({
15291
+ ...previousValue,
15292
+ [currentValue]: true
15293
+ }), {}) : {},
15295
15294
  onSubmit,
15296
15295
  onReset: () => {
15297
15296
  clearDirtyCache();
15298
15297
  onValuesModified?.(false);
15299
15298
  },
15300
- validation: (values_0) => {
15301
- return validationSchema?.validate(values_0, {
15299
+ onValuesChangeDeferred: (values_0, controller) => {
15300
+ const key = status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId;
15301
+ if (controller.dirty) {
15302
+ const touchedValues = extractTouchedValues(values_0, controller.touched);
15303
+ saveEntityToCache(key, touchedValues);
15304
+ }
15305
+ },
15306
+ validation: (values_1) => {
15307
+ return validationSchema?.validate(values_1, {
15302
15308
  abortEarly: false
15303
15309
  }).then(() => {
15304
15310
  return {};
@@ -15381,7 +15387,7 @@ function EntityForm({
15381
15387
  console.error(e_3);
15382
15388
  }, [entityId, path, snackbarController]);
15383
15389
  const saveEntity = ({
15384
- values: values_1,
15390
+ values: values_2,
15385
15391
  previousValues,
15386
15392
  entityId: entityId_0,
15387
15393
  collection: collection_0,
@@ -15390,7 +15396,7 @@ function EntityForm({
15390
15396
  return saveEntityWithCallbacks({
15391
15397
  path: path_0,
15392
15398
  entityId: entityId_0,
15393
- values: values_1,
15399
+ values: values_2,
15394
15400
  previousValues,
15395
15401
  collection: collection_0,
15396
15402
  status,
@@ -15406,34 +15412,34 @@ function EntityForm({
15406
15412
  collection: collection_1,
15407
15413
  path: path_1,
15408
15414
  entityId: entityId_1,
15409
- values: values_2,
15415
+ values: values_3,
15410
15416
  previousValues: previousValues_0,
15411
15417
  autoSave: autoSave_0
15412
15418
  }) => {
15413
15419
  if (!status) return;
15414
15420
  if (autoSave_0) {
15415
- setValuesToBeSaved(values_2);
15421
+ setValuesToBeSaved(values_3);
15416
15422
  } else {
15417
15423
  return saveEntity({
15418
15424
  collection: collection_1,
15419
15425
  path: path_1,
15420
15426
  entityId: entityId_1,
15421
- values: values_2,
15427
+ values: values_3,
15422
15428
  previousValues: previousValues_0
15423
15429
  });
15424
15430
  }
15425
15431
  };
15426
15432
  const lastSavedValues = useRef(entity?.values);
15427
- const save = (values_3) => {
15428
- lastSavedValues.current = values_3;
15433
+ const save = (values_4) => {
15434
+ lastSavedValues.current = values_4;
15429
15435
  return onSaveEntityRequest({
15430
15436
  collection: resolvedCollection,
15431
15437
  path,
15432
15438
  entityId,
15433
- values: values_3,
15439
+ values: values_4,
15434
15440
  previousValues: entity?.values,
15435
15441
  autoSave: autoSave ?? false
15436
- }).then((res) => {
15442
+ }).then(() => {
15437
15443
  const eventName = status === "new" ? "new_entity_saved" : status === "copy" ? "entity_copied" : status === "existing" ? "entity_edited" : "unmapped_event";
15438
15444
  analyticsController.onAnalyticsEvent?.(eventName, {
15439
15445
  path
@@ -15467,7 +15473,8 @@ function EntityForm({
15467
15473
  type: "error",
15468
15474
  message: "Error updating id, check the console"
15469
15475
  });
15470
- }, []);
15476
+ console.error(error);
15477
+ }, [snackbarController]);
15471
15478
  const pluginActions = [];
15472
15479
  const plugins = customizationController.plugins;
15473
15480
  const actionsDisabled = disabled || formex.isSubmitting || status === "existing" && !formex.dirty || Boolean(disabledProp);
@@ -15516,20 +15523,12 @@ function EntityForm({
15516
15523
  onValuesModified?.(modified);
15517
15524
  }
15518
15525
  }, [formex.dirty]);
15519
- const deferredValues = useDeferredValue(formex.values);
15520
15526
  const modified = formex.dirty;
15521
15527
  const uniqueFieldValidator = useCallback(({
15522
15528
  name,
15523
- value,
15524
- property
15529
+ value
15525
15530
  }) => dataSource.checkUniqueField(path, name, value, entityId, collection), [dataSource, path, entityId]);
15526
15531
  const validationSchema = useMemo(() => entityId ? getYupEntitySchema(entityId, resolvedCollection.properties, uniqueFieldValidator) : void 0, [entityId, resolvedCollection.properties, uniqueFieldValidator]);
15527
- useEffect(() => {
15528
- const key = status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId;
15529
- if (modified) {
15530
- saveEntityToCache(key, deferredValues);
15531
- }
15532
- }, [deferredValues, modified, path, entityId, status]);
15533
15532
  useOnAutoSave(autoSave, formex, lastSavedValues, save);
15534
15533
  useEffect(() => {
15535
15534
  if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
@@ -15548,18 +15547,18 @@ function EntityForm({
15548
15547
  return /* @__PURE__ */ jsx(Builder, { collection, entity, modifiedValues: formex.values, formContext });
15549
15548
  }
15550
15549
  return /* @__PURE__ */ jsx(FormLayout, { children: formFieldKeys.map((key_1) => {
15551
- const property_0 = resolvedCollection.properties[key_1];
15552
- if (property_0) {
15550
+ const property = resolvedCollection.properties[key_1];
15551
+ if (property) {
15553
15552
  const underlyingValueHasChanged = !!underlyingChanges && Object.keys(underlyingChanges).includes(key_1) && formex.touched[key_1];
15554
- const disabled_0 = disabledProp || !autoSave && formex.isSubmitting || isReadOnly(property_0) || Boolean(property_0.disabled);
15555
- const hidden = isHidden(property_0);
15553
+ const disabled_0 = disabledProp || !autoSave && formex.isSubmitting || isReadOnly(property) || Boolean(property.disabled);
15554
+ const hidden = isHidden(property);
15556
15555
  if (hidden) return null;
15557
- const widthPercentage = property_0.widthPercentage ?? 100;
15556
+ const widthPercentage = property.widthPercentage ?? 100;
15558
15557
  const cmsFormFieldProps = {
15559
15558
  propertyKey: key_1,
15560
15559
  disabled: disabled_0,
15561
- property: property_0,
15562
- includeDescription: property_0.description || property_0.longDescription,
15560
+ property,
15561
+ includeDescription: property.description || property.longDescription,
15563
15562
  underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
15564
15563
  context: formContext,
15565
15564
  partOfArray: false,
@@ -15615,7 +15614,7 @@ function EntityForm({
15615
15614
  }
15616
15615
  const dialogActions = /* @__PURE__ */ jsx(EntityFormActionsComponent, { collection: resolvedCollection, path, fullPath: path, fullIdPath, entity, layout: forceActionsAtTheBottom ? "bottom" : "side", savingError, formex, disabled: actionsDisabled, status, pluginActions: pluginActions ?? [], openEntityMode, showDefaultActions, navigateBack, formContext });
15617
15616
  return /* @__PURE__ */ jsx(Formex, { value: formex, children: /* @__PURE__ */ jsxs("form", { onSubmit: formex.handleSubmit, onReset: () => formex.resetForm({
15618
- values: getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs)
15617
+ values: baseInitialValues
15619
15618
  }), noValidate: true, className: cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", className), children: [
15620
15619
  /* @__PURE__ */ jsx("div", { id: `form_${path}`, className: cls("relative flex flex-row max-w-4xl lg:max-w-3xl xl:max-w-4xl 2xl:max-w-6xl w-full h-fit"), children: /* @__PURE__ */ jsxs("div", { className: cls("flex flex-col w-full pt-12 pb-16 px-4 sm:px-8 md:px-10"), children: [
15621
15620
  formex.dirty ? /* @__PURE__ */ jsx(Tooltip, { title: "Local unsaved changes", className: "self-end sticky top-4 z-10", children: /* @__PURE__ */ jsx(Chip, { size: "small", colorScheme: "orangeDarker", children: /* @__PURE__ */ jsx(EditIcon, { size: "smallest" }) }) }) : /* @__PURE__ */ jsx(Tooltip, { title: "In sync with the database", className: "self-end sticky top-4 z-10", children: /* @__PURE__ */ jsx(Chip, { size: "small", children: /* @__PURE__ */ jsx(CheckIcon, { size: "smallest" }) }) }),
@@ -22828,14 +22827,14 @@ function EntityEditViewFormActions({
22828
22827
  collection,
22829
22828
  context,
22830
22829
  sideEntityController,
22831
- isSubmitting: formex.isSubmitting,
22832
22830
  disabled,
22833
22831
  status,
22834
22832
  sideDialogContext,
22835
22833
  pluginActions,
22836
22834
  openEntityMode,
22837
22835
  navigateBack,
22838
- formContext
22836
+ formContext,
22837
+ formex
22839
22838
  }) : buildSideActions({
22840
22839
  savingError,
22841
22840
  entity,
@@ -22843,14 +22842,14 @@ function EntityEditViewFormActions({
22843
22842
  collection,
22844
22843
  context,
22845
22844
  sideEntityController,
22846
- isSubmitting: formex.isSubmitting,
22847
22845
  sideDialogContext,
22848
22846
  disabled,
22849
22847
  status,
22850
22848
  pluginActions,
22851
22849
  openEntityMode,
22852
22850
  navigateBack,
22853
- formContext
22851
+ formContext,
22852
+ formex
22854
22853
  });
22855
22854
  }
22856
22855
  function buildBottomActions({
@@ -22860,15 +22859,16 @@ function buildBottomActions({
22860
22859
  collection,
22861
22860
  context,
22862
22861
  sideEntityController,
22863
- isSubmitting,
22864
22862
  disabled,
22865
22863
  status,
22866
22864
  sideDialogContext,
22867
22865
  pluginActions,
22868
22866
  openEntityMode,
22869
22867
  navigateBack,
22870
- formContext
22868
+ formContext,
22869
+ formex
22871
22870
  }) {
22871
+ const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
22872
22872
  const canClose = openEntityMode === "side_panel";
22873
22873
  return /* @__PURE__ */ jsxs(DialogActions, { position: "absolute", children: [
22874
22874
  savingError && /* @__PURE__ */ jsx("div", { className: "text-right", children: /* @__PURE__ */ jsx(Typography, { color: "error", children: savingError.message }) }),
@@ -22888,15 +22888,16 @@ function buildBottomActions({
22888
22888
  return /* @__PURE__ */ jsx(EntityActionButton, { action, enabled: isEnabled, props }, action.key);
22889
22889
  }) }),
22890
22890
  pluginActions,
22891
- /* @__PURE__ */ jsx(Button, { variant: "text", color: "primary", disabled: disabled || isSubmitting, type: "reset", children: status === "existing" ? "Discard" : "Clear" }),
22892
- /* @__PURE__ */ jsxs(Button, { variant: canClose ? "text" : "filled", color: "primary", type: "submit", disabled: disabled || isSubmitting, onClick: () => {
22891
+ hasErrors ? /* @__PURE__ */ jsx(ErrorTooltip, { title: "This form has errors", children: /* @__PURE__ */ jsx(ErrorIcon, { className: "ml-4", color: "error", size: "smallest" }) }) : null,
22892
+ /* @__PURE__ */ jsx(Button, { variant: "text", color: "primary", disabled: disabled || formex.isSubmitting, type: "reset", children: status === "existing" ? "Discard" : "Clear" }),
22893
+ /* @__PURE__ */ jsxs(Button, { variant: canClose ? "text" : "filled", color: "primary", type: "submit", disabled: disabled || formex.isSubmitting, onClick: () => {
22893
22894
  sideDialogContext.setPendingClose(false);
22894
22895
  }, children: [
22895
22896
  status === "existing" && "Save",
22896
22897
  status === "copy" && "Create copy",
22897
22898
  status === "new" && "Create"
22898
22899
  ] }),
22899
- canClose && /* @__PURE__ */ jsxs(LoadingButton, { variant: "filled", color: "primary", type: "submit", loading: isSubmitting, disabled, onClick: () => {
22900
+ canClose && /* @__PURE__ */ jsxs(LoadingButton, { variant: "filled", color: "primary", type: "submit", loading: formex.isSubmitting, disabled, onClick: () => {
22900
22901
  sideDialogContext.setPendingClose?.(true);
22901
22902
  }, children: [
22902
22903
  status === "existing" && "Save and close",
@@ -22912,24 +22913,25 @@ function buildSideActions({
22912
22913
  collection,
22913
22914
  context,
22914
22915
  sideEntityController,
22915
- isSubmitting,
22916
22916
  disabled,
22917
22917
  status,
22918
22918
  sideDialogContext,
22919
22919
  pluginActions,
22920
22920
  openEntityMode,
22921
22921
  navigateBack,
22922
- formContext
22922
+ formContext,
22923
+ formex
22923
22924
  }) {
22925
+ const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
22924
22926
  return /* @__PURE__ */ jsxs("div", { className: cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin), children: [
22925
- /* @__PURE__ */ jsxs(LoadingButton, { fullWidth: true, variant: "filled", color: "primary", type: "submit", size: "large", disabled: disabled || isSubmitting, onClick: () => {
22927
+ /* @__PURE__ */ jsxs(LoadingButton, { fullWidth: true, variant: "filled", color: "primary", type: "submit", size: "large", startIcon: hasErrors ? /* @__PURE__ */ jsx(ErrorIcon, {}) : void 0, disabled: disabled || formex.isSubmitting, onClick: () => {
22926
22928
  sideDialogContext.setPendingClose?.(false);
22927
22929
  }, children: [
22928
22930
  status === "existing" && "Save",
22929
22931
  status === "copy" && "Create copy",
22930
22932
  status === "new" && "Create"
22931
22933
  ] }),
22932
- /* @__PURE__ */ jsx(Button, { fullWidth: true, variant: "text", disabled: disabled || isSubmitting, type: "reset", children: status === "existing" ? "Discard" : "Clear" }),
22934
+ /* @__PURE__ */ jsx(Button, { fullWidth: true, variant: "text", disabled: disabled || formex.isSubmitting, type: "reset", children: status === "existing" ? "Discard" : "Clear" }),
22933
22935
  pluginActions,
22934
22936
  formActions.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-row flex-wrap mt-2", children: formActions.map((action) => {
22935
22937
  const props = {
@@ -23157,7 +23159,8 @@ function EntityEditView({
23157
23159
  databaseId: props.databaseId,
23158
23160
  useCache: false
23159
23161
  });
23160
- const cachedValues = entityId ? getEntityFromCache(props.path + "/" + entityId) : getEntityFromCache(props.path + "#new");
23162
+ const enableLocalChangesBackup = props.collection.enableLocalChangesBackup !== void 0 ? props.collection.enableLocalChangesBackup : true;
23163
+ const initialDirtyValues = entityId ? getEntityFromCache(props.path + "/" + entityId, enableLocalChangesBackup) : getEntityFromCache(props.path + "#new", enableLocalChangesBackup);
23161
23164
  const authController = useAuthController();
23162
23165
  const initialStatus = props.copy ? "copy" : entityId ? "existing" : "new";
23163
23166
  const [status, setStatus] = useState(initialStatus);
@@ -23168,13 +23171,13 @@ function EntityEditView({
23168
23171
  return entity ? canEditEntity(props.collection, authController, props.path, entity ?? null) : void 0;
23169
23172
  }
23170
23173
  }, [authController, entity, status]);
23171
- if (dataLoading && !cachedValues || (!entity || canEdit === void 0) && (status === "existing" || status === "copy")) {
23174
+ if (dataLoading && !initialDirtyValues || (!entity || canEdit === void 0) && (status === "existing" || status === "copy")) {
23172
23175
  return /* @__PURE__ */ jsx(CircularProgressCenter, {});
23173
23176
  }
23174
- if (entityId && !entity && !cachedValues) {
23177
+ if (entityId && !entity && !initialDirtyValues) {
23175
23178
  console.error(`Entity with id ${entityId} not found in collection ${props.path}`);
23176
23179
  }
23177
- return /* @__PURE__ */ jsx(EntityEditViewInner, { ...props, entityId, entity, cachedDirtyValues: cachedValues, dataLoading, status, setStatus, canEdit });
23180
+ return /* @__PURE__ */ jsx(EntityEditViewInner, { ...props, entityId, entity, initialDirtyValues, dataLoading, status, setStatus, canEdit });
23178
23181
  }
23179
23182
  function EntityEditViewInner({
23180
23183
  path,
@@ -23187,7 +23190,7 @@ function EntityEditViewInner({
23187
23190
  onSaved,
23188
23191
  onTabChange,
23189
23192
  entity,
23190
- cachedDirtyValues,
23193
+ initialDirtyValues,
23191
23194
  dataLoading,
23192
23195
  layout = "side_panel",
23193
23196
  barActions,
@@ -23315,7 +23318,7 @@ function EntityEditViewInner({
23315
23318
  /* @__PURE__ */ jsx(EntityView, { className: "px-8 h-full overflow-auto", entity, path, collection }),
23316
23319
  /* @__PURE__ */ jsx("div", { className: "h-16" })
23317
23320
  ] }) }) : null;
23318
- const entityView = /* @__PURE__ */ jsx(EntityForm, { fullIdPath, collection, path, entityId: entityId ?? usedEntity?.id, onValuesModified, entity, initialDirtyValues: cachedDirtyValues, openEntityMode: layout, forceActionsAtTheBottom: actionsAtTheBottom, initialStatus: status, className: cls((!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : "", formProps?.className), EntityFormActionsComponent: EntityEditViewFormActions, disabled: !canEdit, ...formProps, onEntityChange: (entity_0) => {
23321
+ const entityView = /* @__PURE__ */ jsx(EntityForm, { fullIdPath, collection, path, entityId: entityId ?? usedEntity?.id, onValuesModified, entity, initialDirtyValues, openEntityMode: layout, forceActionsAtTheBottom: actionsAtTheBottom, initialStatus: status, className: cls((!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : "", formProps?.className), EntityFormActionsComponent: EntityEditViewFormActions, disabled: !canEdit, ...formProps, onEntityChange: (entity_0) => {
23319
23322
  setUsedEntity(entity_0);
23320
23323
  formProps?.onEntityChange?.(entity_0);
23321
23324
  }, onStatusChange: (status_0) => {