@firecms/core 3.1.0-canary.9e89e98 → 3.1.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.
Files changed (43) hide show
  1. package/dist/components/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
  2. package/dist/components/ErrorBoundary.d.ts +1 -1
  3. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +1 -1
  4. package/dist/form/components/ErrorFocus.d.ts +1 -1
  5. package/dist/index.es.js +118 -54
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +118 -54
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/internal/useRestoreScroll.d.ts +1 -1
  10. package/dist/types/analytics.d.ts +1 -1
  11. package/dist/types/plugins.d.ts +16 -0
  12. package/dist/util/entities.d.ts +1 -1
  13. package/dist/util/resolutions.d.ts +2 -2
  14. package/package.json +9 -9
  15. package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
  16. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
  17. package/src/components/EntityCollectionView/EntityBoardCard.tsx +1 -1
  18. package/src/components/EntityCollectionView/EntityCard.tsx +4 -0
  19. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +23 -3
  20. package/src/components/EntityCollectionView/EntityCollectionView.tsx +32 -3
  21. package/src/components/VirtualTable/VirtualTable.tsx +116 -113
  22. package/src/components/VirtualTable/VirtualTableHeader.tsx +42 -42
  23. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +1 -1
  24. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +3 -3
  25. package/src/core/DefaultAppBar.tsx +1 -1
  26. package/src/core/EntitySidePanel.tsx +28 -26
  27. package/src/core/field_configs.tsx +14 -9
  28. package/src/form/EntityForm.tsx +69 -60
  29. package/src/form/PropertyFieldBinding.tsx +3 -3
  30. package/src/form/components/ErrorFocus.tsx +3 -3
  31. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +1 -1
  32. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +83 -83
  33. package/src/hooks/useBuildNavigationController.tsx +4 -4
  34. package/src/hooks/useValidateAuthenticator.tsx +1 -1
  35. package/src/internal/useBuildDataSource.ts +1 -2
  36. package/src/preview/PropertyPreview.tsx +1 -0
  37. package/src/types/analytics.ts +10 -0
  38. package/src/types/plugins.tsx +18 -0
  39. package/src/util/entities.ts +1 -1
  40. package/src/util/join_collections.ts +10 -8
  41. package/src/util/previews.ts +2 -2
  42. package/src/util/property_utils.tsx +1 -1
  43. package/src/util/resolutions.ts +5 -3
@@ -181,30 +181,30 @@ export function getChanges<T extends object>(source: Partial<T>, comparison: Par
181
181
  }
182
182
 
183
183
  export function EntityForm<M extends Record<string, any>>({
184
- path,
185
- fullIdPath,
186
- entityId: entityIdProp,
187
- collection,
188
- onValuesModified,
189
- onIdChange,
190
- onSaved,
191
- entity,
192
- initialDirtyValues,
193
- onFormContextReady,
194
- forceActionsAtTheBottom,
195
- initialStatus,
196
- className,
197
- onStatusChange,
198
- onEntityChange,
199
- openEntityMode = "full_screen",
200
- formex: formexProp,
201
- disabled: disabledProp,
202
- Builder,
203
- EntityFormActionsComponent = EntityFormActions,
204
- showDefaultActions = true,
205
- showEntityPath = true,
206
- children
207
- }: EntityFormProps<M>) {
184
+ path,
185
+ fullIdPath,
186
+ entityId: entityIdProp,
187
+ collection,
188
+ onValuesModified,
189
+ onIdChange,
190
+ onSaved,
191
+ entity,
192
+ initialDirtyValues,
193
+ onFormContextReady,
194
+ forceActionsAtTheBottom,
195
+ initialStatus,
196
+ className,
197
+ onStatusChange,
198
+ onEntityChange,
199
+ openEntityMode = "full_screen",
200
+ formex: formexProp,
201
+ disabled: disabledProp,
202
+ Builder,
203
+ EntityFormActionsComponent = EntityFormActions,
204
+ showDefaultActions = true,
205
+ showEntityPath = true,
206
+ children
207
+ }: EntityFormProps<M>) {
208
208
 
209
209
  if (collection.customId && collection.formAutoSave) {
210
210
  console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
@@ -455,12 +455,12 @@ export function EntityForm<M extends Record<string, any>>({
455
455
  }, [entityId, path, snackbarController]);
456
456
 
457
457
  const saveEntity = ({
458
- values,
459
- previousValues,
460
- entityId,
461
- collection,
462
- path
463
- }: {
458
+ values,
459
+ previousValues,
460
+ entityId,
461
+ collection,
462
+ path
463
+ }: {
464
464
  collection: EntityCollection<M>,
465
465
  path: string,
466
466
  entityId: string | undefined,
@@ -493,13 +493,13 @@ export function EntityForm<M extends Record<string, any>>({
493
493
  };
494
494
 
495
495
  const onSaveEntityRequest = async ({
496
- collection,
497
- path,
498
- entityId,
499
- values,
500
- previousValues,
501
- autoSave
502
- }: EntityFormSaveParams<M>): Promise<void> => {
496
+ collection,
497
+ path,
498
+ entityId,
499
+ values,
500
+ previousValues,
501
+ autoSave
502
+ }: EntityFormSaveParams<M>): Promise<void> => {
503
503
  if (!status)
504
504
  return;
505
505
  if (autoSave) {
@@ -567,6 +567,7 @@ export function EntityForm<M extends Record<string, any>>({
567
567
  }, [snackbarController]);
568
568
 
569
569
  const pluginActions: React.ReactNode[] = [];
570
+ const pluginBeforeTitle: React.ReactNode[] = [];
570
571
  const plugins = customizationController.plugins;
571
572
 
572
573
  const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
@@ -590,6 +591,12 @@ export function EntityForm<M extends Record<string, any>>({
590
591
  key={`actions_${plugin.key}`} {...actionProps} />
591
592
  : null
592
593
  )).filter(Boolean));
594
+ pluginBeforeTitle.push(...plugins.map((plugin) => (
595
+ plugin.form?.BeforeTitle
596
+ ? <plugin.form.BeforeTitle
597
+ key={`before_title_${plugin.key}`} {...actionProps} />
598
+ : null
599
+ )).filter(Boolean));
593
600
  }
594
601
 
595
602
  const titlePropertyKey = getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs);
@@ -631,17 +638,17 @@ export function EntityForm<M extends Record<string, any>>({
631
638
  const modified = formex.dirty;
632
639
 
633
640
  const uniqueFieldValidator: CustomFieldValidator = useCallback(({
634
- name,
635
- value
636
- }) => dataSource.checkUniqueField(path, name, value, entityId, collection),
641
+ name,
642
+ value
643
+ }) => dataSource.checkUniqueField(path, name, value, entityId, collection),
637
644
  [dataSource, path, entityId]);
638
645
 
639
646
  const validationSchema = useMemo(() => entityId
640
- ? getYupEntitySchema(
641
- entityId,
642
- resolvedCollection.properties,
643
- uniqueFieldValidator)
644
- : undefined,
647
+ ? getYupEntitySchema(
648
+ entityId,
649
+ resolvedCollection.properties,
650
+ uniqueFieldValidator)
651
+ : undefined,
645
652
  [entityId, resolvedCollection.properties, uniqueFieldValidator]);
646
653
 
647
654
  useOnAutoSave(autoSave, formex, lastSavedValues, save);
@@ -699,8 +706,8 @@ export function EntityForm<M extends Record<string, any>>({
699
706
 
700
707
  return (
701
708
  <FormEntry propertyKey={key}
702
- widthPercentage={widthPercentage}
703
- key={`field_${key}`}>
709
+ widthPercentage={widthPercentage}
710
+ key={`field_${key}`}>
704
711
  <PropertyFieldBinding {...cmsFormFieldProps} />
705
712
  </FormEntry>
706
713
  );
@@ -713,7 +720,7 @@ export function EntityForm<M extends Record<string, any>>({
713
720
  throw new Error("When using additional fields you need to provide a Builder or a value");
714
721
  }
715
722
  const child = Builder
716
- ? <Builder entity={entity} context={context}/>
723
+ ? <Builder entity={entity} context={context} />
717
724
  : <div className={"w-full"}>
718
725
  {additionalField.value?.({
719
726
  entity,
@@ -725,9 +732,9 @@ export function EntityForm<M extends Record<string, any>>({
725
732
  <div key={`additional_${key}`} className={"w-full"}>
726
733
  <LabelWithIconAndTooltip
727
734
  propertyKey={key}
728
- icon={<NotesIcon size={"small"}/>}
735
+ icon={<NotesIcon size={"small"} />}
729
736
  title={additionalField.name}
730
- className={"text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>
737
+ className={"text-text-secondary dark:text-text-secondary-dark ml-3.5"} />
731
738
  <div
732
739
  className={cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-scroll no-scrollbar")}>
733
740
  <ErrorBoundary>
@@ -749,6 +756,8 @@ export function EntityForm<M extends Record<string, any>>({
749
756
 
750
757
  const formView = <ErrorBoundary>
751
758
  <>
759
+ {pluginBeforeTitle}
760
+
752
761
  {!Builder && <div className={"w-full py-2 flex flex-col items-start my-4 lg:my-6"}>
753
762
  <Typography
754
763
  className={"my-4 flex-grow line-clamp-1 " + (collection.hideIdFromForm ? "mb-6" : "")}
@@ -777,22 +786,22 @@ export function EntityForm<M extends Record<string, any>>({
777
786
 
778
787
  {!Builder && !collection.hideIdFromForm &&
779
788
  <CustomIdField customId={collection.customId}
780
- entityId={entityId}
781
- status={status}
782
- onChange={setEntityId}
783
- error={entityIdError}
784
- loading={customIdLoading}
785
- entity={entity}/>
789
+ entityId={entityId}
790
+ status={status}
791
+ onChange={setEntityId}
792
+ error={entityIdError}
793
+ loading={customIdLoading}
794
+ entity={entity} />
786
795
  }
787
796
 
788
797
  {entityId && formContext && <>
789
798
  <div className="mt-12 flex flex-col gap-8" ref={formRef}>
790
799
  {formFields()}
791
- <ErrorFocus containerRef={formRef}/>
800
+ <ErrorFocus containerRef={formRef} />
792
801
  </div>
793
802
  </>}
794
803
 
795
- {forceActionsAtTheBottom && <div className="h-16"/>}
804
+ {forceActionsAtTheBottom && <div className="h-16" />}
796
805
  </>
797
806
  </ErrorBoundary>;
798
807
 
@@ -852,12 +861,12 @@ export function EntityForm<M extends Record<string, any>>({
852
861
  {formex.dirty
853
862
  ? <Tooltip title={"This form has been modified"}>
854
863
  <Chip size={"small"} className={"py-1"} colorScheme={"orangeDarker"}>
855
- <EditIcon size={"smallest"}/>
864
+ <EditIcon size={"smallest"} />
856
865
  </Chip>
857
866
  </Tooltip>
858
867
  : <Tooltip title={"The current form is in sync with the database"}>
859
868
  <Chip size={"small"} className={"py-1"}>
860
- <CheckIcon size={"smallest"}/>
869
+ <CheckIcon size={"smallest"} />
861
870
  </Chip>
862
871
  </Tooltip>}
863
872
  </div>
@@ -137,7 +137,7 @@ function PropertyFieldBindingInternal<T extends CMSType = CMSType, M extends Rec
137
137
  }
138
138
  const configProperty = resolveProperty({
139
139
  propertyKey,
140
- propertyOrBuilder: propertyConfig.property,
140
+ propertyOrBuilder: propertyConfig.property as any,
141
141
  values: fieldProps.form.values,
142
142
  path: context.path,
143
143
  entityId: context.entityId,
@@ -145,7 +145,7 @@ function PropertyFieldBindingInternal<T extends CMSType = CMSType, M extends Rec
145
145
  index,
146
146
  authController
147
147
  });
148
- Component = configProperty.Field as ComponentType<FieldProps<T>>;
148
+ Component = configProperty?.Field as ComponentType<FieldProps<T>> | undefined;
149
149
  }
150
150
  if (!Component) {
151
151
  console.warn(`No field component found for property ${propertyKey}`);
@@ -302,7 +302,7 @@ const shouldPropertyReRender = (property: PropertyOrBuilder | ResolvedProperty,
302
302
  if (plugins?.some((plugin) => plugin.form?.fieldBuilder)) {
303
303
  return true;
304
304
  }
305
- if (isPropertyBuilder(property)) {
305
+ if (isPropertyBuilder(property as any)) {
306
306
  return true;
307
307
  }
308
308
  const defAProperty = property as Property | ResolvedProperty;
@@ -2,9 +2,9 @@ import React, { useEffect, useRef } from "react";
2
2
  import { useFormex } from "@firecms/formex";
3
3
 
4
4
  export const ErrorFocus = ({ containerRef }:
5
- {
6
- containerRef?: React.RefObject<HTMLDivElement>
7
- }) => {
5
+ {
6
+ containerRef?: React.RefObject<HTMLDivElement | null>
7
+ }) => {
8
8
  const {
9
9
  isValidating,
10
10
  errors,
@@ -65,7 +65,7 @@ export function MarkdownEditorFieldBinding({
65
65
  }, [value]);
66
66
 
67
67
  const resolvedProperty = resolveProperty({
68
- propertyOrBuilder: property as PropertyOrBuilder,
68
+ propertyOrBuilder: property as PropertyOrBuilder<string>,
69
69
  values: entityValues,
70
70
  authController
71
71
  }) as ResolvedStringProperty | ResolvedArrayProperty<string[]>;
@@ -52,18 +52,18 @@ const rejectDropClasses = "transition-colors duration-200 ease-[cubic-bezier(0,0
52
52
  type StorageUploadFieldProps = FieldProps<string | string[]>;
53
53
 
54
54
  export function StorageUploadFieldBinding({
55
- propertyKey,
56
- value,
57
- setValue,
58
- error,
59
- showError,
60
- autoFocus,
61
- minimalistView,
62
- property,
63
- includeDescription,
64
- context,
65
- isSubmitting,
66
- }: StorageUploadFieldProps) {
55
+ propertyKey,
56
+ value,
57
+ setValue,
58
+ error,
59
+ showError,
60
+ autoFocus,
61
+ minimalistView,
62
+ property,
63
+ includeDescription,
64
+ context,
65
+ isSubmitting,
66
+ }: StorageUploadFieldProps) {
67
67
 
68
68
  const authController = useAuthController();
69
69
 
@@ -100,7 +100,7 @@ export function StorageUploadFieldBinding({
100
100
  });
101
101
 
102
102
  const resolvedProperty = resolveProperty({
103
- propertyOrBuilder: property as PropertyOrBuilder,
103
+ propertyOrBuilder: property as PropertyOrBuilder<string>,
104
104
  authController
105
105
  }) as ResolvedStringProperty | ResolvedArrayProperty<string[]>;
106
106
 
@@ -114,7 +114,7 @@ export function StorageUploadFieldBinding({
114
114
  icon={getIconForProperty(property, "small")}
115
115
  required={property.validation?.required}
116
116
  title={property.name}
117
- className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>}
117
+ className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"} />}
118
118
 
119
119
  <StorageUpload
120
120
  value={internalValue}
@@ -128,13 +128,13 @@ export function StorageUploadFieldBinding({
128
128
  onFileUploadComplete={onFileUploadComplete}
129
129
  storagePathBuilder={storagePathBuilder}
130
130
  storage={storage}
131
- multipleFilesSupported={multipleFilesSupported}/>
131
+ multipleFilesSupported={multipleFilesSupported} />
132
132
 
133
133
  <FieldHelperText includeDescription={includeDescription}
134
- showError={showError}
135
- error={error}
136
- disabled={disabled}
137
- property={property}/>
134
+ showError={showError}
135
+ error={error}
136
+ disabled={disabled}
137
+ property={property} />
138
138
 
139
139
  </>
140
140
  );
@@ -154,15 +154,15 @@ interface SortableStorageItemProps {
154
154
  }
155
155
 
156
156
  function SortableStorageItem({
157
- id,
158
- entry,
159
- property,
160
- metadata,
161
- storagePathBuilder,
162
- onFileUploadComplete,
163
- onClear,
164
- disabled,
165
- }: SortableStorageItemProps) {
157
+ id,
158
+ entry,
159
+ property,
160
+ metadata,
161
+ storagePathBuilder,
162
+ onFileUploadComplete,
163
+ onClear,
164
+ disabled,
165
+ }: SortableStorageItemProps) {
166
166
 
167
167
  const {
168
168
  attributes,
@@ -201,7 +201,7 @@ function SortableStorageItem({
201
201
  disabled={disabled}
202
202
  value={entry.storagePathOrDownloadUrl}
203
203
  onRemove={() => onClear(entry.storagePathOrDownloadUrl!)}
204
- size={entry.size}/>
204
+ size={entry.size} />
205
205
  );
206
206
  } else if (entry.file) {
207
207
  child = (
@@ -231,21 +231,21 @@ function SortableStorageItem({
231
231
  }
232
232
 
233
233
  function FileDropComponent({
234
- storage,
235
- disabled,
236
- onFilesAdded,
237
- multipleFilesSupported,
238
- autoFocus,
239
- internalValue,
240
- property,
241
- onClear,
242
- metadata,
243
- storagePathBuilder,
244
- onFileUploadComplete,
245
- name,
246
- helpText,
247
- isDndItemDragging
248
- }: {
234
+ storage,
235
+ disabled,
236
+ onFilesAdded,
237
+ multipleFilesSupported,
238
+ autoFocus,
239
+ internalValue,
240
+ property,
241
+ onClear,
242
+ metadata,
243
+ storagePathBuilder,
244
+ onFileUploadComplete,
245
+ name,
246
+ helpText,
247
+ isDndItemDragging
248
+ }: {
249
249
  storage: StorageConfig,
250
250
  disabled: boolean,
251
251
  onFilesAdded: (acceptedFiles: File[]) => Promise<void>,
@@ -271,33 +271,33 @@ function FileDropComponent({
271
271
  isDragAccept,
272
272
  isDragReject
273
273
  } = useDropzone({
274
- accept: storage.acceptedFiles ? storage.acceptedFiles.reduce((acc, ext) => ({
275
- ...acc,
276
- [ext]: []
277
- }), {}) : undefined,
278
- disabled: disabled || isDndItemDragging,
279
- noDragEventsBubbling: true,
280
- maxSize: storage.maxSize,
281
- onDrop: onFilesAdded,
282
- onDropRejected: (fileRejections) => {
283
- for (const fileRejection of fileRejections) {
284
- for (const error of fileRejection.errors) {
285
- console.error("Error uploading file: ", error);
286
- if (error.code === "file-too-large") {
287
- snackbarContext.open({
288
- type: "error",
289
- message: `Error uploading file: File is larger than ${storage.maxSize} bytes`
290
- });
291
- } else if (error.code === "file-invalid-type") {
292
- snackbarContext.open({
293
- type: "error",
294
- message: "Error uploading file: File type is not supported"
295
- });
296
- }
274
+ accept: storage.acceptedFiles ? storage.acceptedFiles.reduce((acc, ext) => ({
275
+ ...acc,
276
+ [ext]: []
277
+ }), {}) : undefined,
278
+ disabled: disabled || isDndItemDragging,
279
+ noDragEventsBubbling: true,
280
+ maxSize: storage.maxSize,
281
+ onDrop: onFilesAdded,
282
+ onDropRejected: (fileRejections) => {
283
+ for (const fileRejection of fileRejections) {
284
+ for (const error of fileRejection.errors) {
285
+ console.error("Error uploading file: ", error);
286
+ if (error.code === "file-too-large") {
287
+ snackbarContext.open({
288
+ type: "error",
289
+ message: `Error uploading file: File is larger than ${storage.maxSize} bytes`
290
+ });
291
+ } else if (error.code === "file-invalid-type") {
292
+ snackbarContext.open({
293
+ type: "error",
294
+ message: "Error uploading file: File type is not supported"
295
+ });
297
296
  }
298
297
  }
299
298
  }
300
299
  }
300
+ }
301
301
  );
302
302
 
303
303
  return (
@@ -349,8 +349,8 @@ function FileDropComponent({
349
349
  <div
350
350
  className="flex-grow min-h-[38px] box-border m-2 text-center">
351
351
  <Typography align={"center"}
352
- variant={"label"}
353
- className={disabled ? "text-surface-accent-600 dark:text-surface-accent-500" : ""}>
352
+ variant={"label"}
353
+ className={disabled ? "text-surface-accent-600 dark:text-surface-accent-500" : ""}>
354
354
  {helpText}
355
355
  </Typography>
356
356
  </div>
@@ -374,19 +374,19 @@ export interface StorageUploadProps {
374
374
  }
375
375
 
376
376
  export function StorageUpload({
377
- property,
378
- name,
379
- value, // This is internalValue from useStorageUploadController
380
- setInternalValue,
381
- onChange,
382
- multipleFilesSupported,
383
- onFileUploadComplete,
384
- disabled,
385
- onFilesAdded,
386
- autoFocus,
387
- storage,
388
- storagePathBuilder,
389
- }: StorageUploadProps) {
377
+ property,
378
+ name,
379
+ value, // This is internalValue from useStorageUploadController
380
+ setInternalValue,
381
+ onChange,
382
+ multipleFilesSupported,
383
+ onFileUploadComplete,
384
+ disabled,
385
+ onFilesAdded,
386
+ autoFocus,
387
+ storage,
388
+ storagePathBuilder,
389
+ }: StorageUploadProps) {
390
390
 
391
391
  if (multipleFilesSupported) {
392
392
  const arrayProperty = property as ResolvedArrayProperty<string[]>;
@@ -500,6 +500,6 @@ export function StorageUpload({
500
500
  );
501
501
  } else {
502
502
  // For single file, no D&D context is needed
503
- return <FileDropComponent {...fileDropProps} isDndItemDragging={false}/>;
503
+ return <FileDropComponent {...fileDropProps} isDndItemDragging={false} />;
504
504
  }
505
505
  }
@@ -122,10 +122,10 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
122
122
 
123
123
  const navigate = useNavigate();
124
124
 
125
- const collectionsRef = useRef<EntityCollection[] | undefined>();
126
- const viewsRef = useRef<CMSView[] | undefined>();
127
- const adminViewsRef = useRef<CMSView[] | undefined>();
128
- const navigationEntriesOrderRef = useRef<string[] | undefined>();
125
+ const collectionsRef = useRef<EntityCollection[] | undefined>(undefined);
126
+ const viewsRef = useRef<CMSView[] | undefined>(undefined);
127
+ const adminViewsRef = useRef<CMSView[] | undefined>(undefined);
128
+ const navigationEntriesOrderRef = useRef<string[] | undefined>(undefined);
129
129
 
130
130
  const [initialised, setInitialised] = useState<boolean>(false);
131
131
 
@@ -51,7 +51,7 @@ export function useValidateAuthenticator<USER extends User = any>
51
51
  * We use this ref to check the authentication only if the user has
52
52
  * changed.
53
53
  */
54
- const checkedUserRef = useRef<User | undefined>();
54
+ const checkedUserRef = useRef<User | undefined>(undefined);
55
55
 
56
56
  const checkAuthentication = useCallback(async () => {
57
57
 
@@ -238,8 +238,7 @@ export function useBuildDataSource({
238
238
  const orderProperty = collection?.orderProperty;
239
239
  if (orderProperty && (status === "new" || status === "copy")) {
240
240
  const orderProp = properties?.[orderProperty as keyof M];
241
- // Only auto-assign if property is disabled (automatic mode)
242
- if (orderProp?.disabled === true) {
241
+ if (orderProp) {
243
242
  const currentValue = updatedValues[orderProperty as keyof M];
244
243
  if (currentValue === undefined || currentValue === null) {
245
244
  try {
@@ -186,6 +186,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<T extends CMS
186
186
  if (typeof value === "object") {
187
187
  content =
188
188
  <MapPropertyPreview {...props}
189
+ value={value as Record<string, CMSType>}
189
190
  property={property as ResolvedMapProperty} />;
190
191
  } else {
191
192
  content = buildWrongValueType(propertyKey, property.dataType, value);
@@ -34,5 +34,15 @@ export type CMSAnalyticsEvent =
34
34
 
35
35
  | "collection_inline_editing"
36
36
 
37
+ | "view_mode_changed"
38
+
39
+ | "kanban_card_moved"
40
+ | "kanban_column_reorder"
41
+ | "kanban_property_changed"
42
+ | "kanban_new_entity_in_column"
43
+ | "kanban_backfill_order"
44
+
45
+ | "card_view_entity_click"
46
+
37
47
  | "unmapped_event"
38
48
  ;
@@ -112,6 +112,19 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
112
112
 
113
113
  collectionView?: {
114
114
 
115
+ /**
116
+ * Custom component to render when a collection loading error occurs.
117
+ * If provided, this replaces the default error view in all collection view modes
118
+ * (table, card, kanban).
119
+ * Return `null` from the component to fall back to the default error view.
120
+ */
121
+ CollectionError?: React.ComponentType<{
122
+ path: string;
123
+ collection: EC;
124
+ parentCollectionIds?: string[];
125
+ error: Error;
126
+ }>;
127
+
115
128
  /**
116
129
  * Use this component to add custom actions to the entity collections
117
130
  * toolbar.
@@ -229,6 +242,11 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
229
242
  */
230
243
  ActionsTop?: React.ComponentType<PluginFormActionProps<any, EC>>;
231
244
 
245
+ /**
246
+ * Add custom content above the entity title in the form view
247
+ */
248
+ BeforeTitle?: React.ComponentType<PluginFormActionProps<any, EC>>;
249
+
232
250
  fieldBuilder?: <T extends CMSType = CMSType>(props: PluginFieldBuilderParams<T, any, EC>) => React.ComponentType<FieldProps<T>> | null;
233
251
 
234
252
  fieldBuilderEnabled?: <T extends CMSType = CMSType>(props: PluginFieldBuilderParams<T>) => boolean;
@@ -33,7 +33,7 @@ export function isHidden(property: Property | ResolvedProperty): boolean {
33
33
  return typeof property.disabled === "object" && Boolean(property.disabled.hidden);
34
34
  }
35
35
 
36
- export function isPropertyBuilder<T extends CMSType, M extends Record<string, any>>(propertyOrBuilder?: PropertyOrBuilder<T, M> | Property<T> | ResolvedProperty<T>): propertyOrBuilder is PropertyBuilder<T, M> {
36
+ export function isPropertyBuilder<T extends CMSType = CMSType, M extends Record<string, any> = any>(propertyOrBuilder?: PropertyOrBuilder<T, M> | Property | ResolvedProperty): propertyOrBuilder is PropertyBuilder<T, M> {
37
37
  return typeof propertyOrBuilder === "function";
38
38
  }
39
39