@firecms/core 3.1.0-canary.1df3b2c → 3.1.0-canary.24c8270

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 (50) hide show
  1. package/dist/components/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
  2. package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +5 -10
  3. package/dist/components/ErrorBoundary.d.ts +1 -1
  4. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +1 -1
  5. package/dist/form/components/ErrorFocus.d.ts +1 -1
  6. package/dist/index.es.js +302 -227
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +300 -225
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/internal/useRestoreScroll.d.ts +1 -1
  11. package/dist/types/analytics.d.ts +1 -1
  12. package/dist/types/collections.d.ts +8 -0
  13. package/dist/types/plugins.d.ts +16 -0
  14. package/dist/util/entities.d.ts +1 -1
  15. package/dist/util/resolutions.d.ts +2 -2
  16. package/package.json +9 -9
  17. package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
  18. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
  19. package/src/components/EntityCollectionView/EntityBoardCard.tsx +1 -1
  20. package/src/components/EntityCollectionView/EntityCard.tsx +4 -0
  21. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +23 -3
  22. package/src/components/EntityCollectionView/EntityCollectionView.tsx +50 -16
  23. package/src/components/EntityCollectionView/ViewModeToggle.tsx +27 -30
  24. package/src/components/VirtualTable/VirtualTable.tsx +116 -113
  25. package/src/components/VirtualTable/VirtualTableHeader.tsx +42 -42
  26. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +1 -1
  27. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +3 -3
  28. package/src/components/common/useDataSourceTableController.tsx +21 -4
  29. package/src/core/DefaultAppBar.tsx +1 -1
  30. package/src/core/EntityEditView.tsx +1 -1
  31. package/src/core/EntitySidePanel.tsx +28 -26
  32. package/src/core/field_configs.tsx +14 -9
  33. package/src/form/EntityForm.tsx +69 -60
  34. package/src/form/PropertyFieldBinding.tsx +3 -3
  35. package/src/form/components/ErrorFocus.tsx +3 -3
  36. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +1 -1
  37. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +83 -83
  38. package/src/hooks/useBuildNavigationController.tsx +29 -9
  39. package/src/hooks/useValidateAuthenticator.tsx +1 -1
  40. package/src/internal/useBuildDataSource.ts +1 -2
  41. package/src/internal/useBuildSideEntityController.tsx +22 -20
  42. package/src/preview/PropertyPreview.tsx +1 -0
  43. package/src/types/analytics.ts +10 -0
  44. package/src/types/collections.ts +9 -0
  45. package/src/types/plugins.tsx +18 -0
  46. package/src/util/entities.ts +1 -1
  47. package/src/util/join_collections.ts +10 -8
  48. package/src/util/previews.ts +2 -2
  49. package/src/util/property_utils.tsx +1 -1
  50. package/src/util/resolutions.ts +5 -3
@@ -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
 
@@ -487,8 +487,10 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
487
487
 
488
488
  const urlPathToDataPath = useCallback((path: string): string => {
489
489
  const decodedPath = decodeURIComponent(path);
490
- if (decodedPath.startsWith(fullCollectionPath))
491
- return decodedPath.replace(fullCollectionPath, "");
490
+ const withoutHash = decodedPath.split("#")[0];
491
+ const cleanPath = withoutHash.split("?")[0];
492
+ if (cleanPath.startsWith(fullCollectionPath))
493
+ return cleanPath.replace(fullCollectionPath, "");
492
494
  throw Error("Expected path starting with " + fullCollectionPath);
493
495
  }, [fullCollectionPath]);
494
496
 
@@ -565,9 +567,27 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
565
567
  }
566
568
 
567
569
  function encodePath(input: string) {
568
- return encodeURIComponent(removeInitialAndTrailingSlashes(input))
569
- .replaceAll("%2F", "/")
570
- .replaceAll("%23", "#");
570
+ const cleanInput = removeInitialAndTrailingSlashes(input);
571
+ const [pathPart, rest] = cleanInput.split("?", 2);
572
+
573
+ let encodedPath = encodeURIComponent(pathPart).replaceAll("%2F", "/");
574
+ let result = encodedPath;
575
+
576
+ if (rest !== undefined) {
577
+ const [searchPart, hashPart] = rest.split("#", 2);
578
+ result += `?${searchPart}`;
579
+ if (hashPart !== undefined) {
580
+ result += `#${hashPart}`;
581
+ }
582
+ } else {
583
+ const [pathOnly, hashOnly] = cleanInput.split("#", 2);
584
+ if (hashOnly !== undefined) {
585
+ encodedPath = encodeURIComponent(pathOnly).replaceAll("%2F", "/");
586
+ result = `${encodedPath}#${hashOnly}`;
587
+ }
588
+ }
589
+
590
+ return result;
571
591
  }
572
592
 
573
593
  function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[], authController: AuthController<User>): EntityCollection[] {
@@ -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 {
@@ -99,8 +99,8 @@ function getNestedPropertiesDepth(property: ResolvedProperty, accumulator: numbe
99
99
  }
100
100
 
101
101
  export const useBuildSideEntityController = (navigation: NavigationController,
102
- sideDialogsController: SideDialogsController,
103
- authController: AuthController
102
+ sideDialogsController: SideDialogsController,
103
+ authController: AuthController
104
104
  ): SideEntityController => {
105
105
 
106
106
  const location = useLocation();
@@ -121,9 +121,9 @@ export const useBuildSideEntityController = (navigation: NavigationController,
121
121
  for (let i = 0; i < panelsFromUrl.length; i++) {
122
122
  const props = panelsFromUrl[i];
123
123
  if (i === 0)
124
- sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController));
124
+ sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search));
125
125
  else
126
- sideDialogsController.open(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController))
126
+ sideDialogsController.open(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search))
127
127
  }
128
128
  }
129
129
  initialised.current = true;
@@ -143,7 +143,7 @@ export const useBuildSideEntityController = (navigation: NavigationController,
143
143
  return;
144
144
  }
145
145
  const lastPanel = panelsFromUrl[panelsFromUrl.length - 1];
146
- const panelProps = propsToSidePanel(lastPanel, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController);
146
+ const panelProps = propsToSidePanel(lastPanel, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search);
147
147
  const lastCurrentPanel = currentPanelKeys.length > 0 ? currentPanelKeys[currentPanelKeys.length - 1] : undefined;
148
148
  if (!lastCurrentPanel || lastCurrentPanel !== panelProps.key) {
149
149
  sideDialogsController.replace(panelProps);
@@ -156,7 +156,7 @@ export const useBuildSideEntityController = (navigation: NavigationController,
156
156
  useEffect(() => {
157
157
  const updatedSidePanels = sideDialogsController.sidePanels.map(sidePanelProps => {
158
158
  if (sidePanelProps.additional) {
159
- return propsToSidePanel(sidePanelProps.additional, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController);
159
+ return propsToSidePanel(sidePanelProps.additional, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search);
160
160
  }
161
161
  return sidePanelProps;
162
162
  });
@@ -183,17 +183,18 @@ export const useBuildSideEntityController = (navigation: NavigationController,
183
183
 
184
184
  sideDialogsController.open(
185
185
  propsToSidePanel({
186
- selectedTab: defaultSelectedView,
187
- ...props
188
- },
186
+ selectedTab: defaultSelectedView,
187
+ ...props
188
+ },
189
189
  navigation.buildUrlCollectionPath,
190
190
  navigation.resolveIdsFrom,
191
191
  smallLayout,
192
192
  customizationController,
193
- authController
193
+ authController,
194
+ location.search
194
195
  ));
195
196
 
196
- }, [sideDialogsController, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, authController.user]);
197
+ }, [sideDialogsController, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, authController.user, location.search]);
197
198
 
198
199
  const replace = useCallback((props: EntitySidePanelProps<any>) => {
199
200
 
@@ -201,9 +202,9 @@ export const useBuildSideEntityController = (navigation: NavigationController,
201
202
  throw Error("If you want to copy an entity you need to provide an entityId");
202
203
  }
203
204
 
204
- sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController));
205
+ sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search));
205
206
 
206
- }, [navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, sideDialogsController, smallLayout, authController.user]);
207
+ }, [navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, sideDialogsController, smallLayout, authController.user, location.search]);
207
208
 
208
209
  return {
209
210
  close,
@@ -265,18 +266,19 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
265
266
  }
266
267
 
267
268
  const propsToSidePanel = (props: EntitySidePanelProps,
268
- buildUrlCollectionPath: (path: string) => string,
269
- resolveIdsFrom: (pathWithAliases: string) => string,
270
- smallLayout: boolean,
271
- customizationController: CustomizationController,
272
- authController: AuthController
269
+ buildUrlCollectionPath: (path: string) => string,
270
+ resolveIdsFrom: (pathWithAliases: string) => string,
271
+ smallLayout: boolean,
272
+ customizationController: CustomizationController,
273
+ authController: AuthController,
274
+ locationSearch: string
273
275
  ): SideDialogPanelProps => {
274
276
 
275
277
  const collectionPath = removeInitialAndTrailingSlashes(props.path);
276
278
 
277
279
  const urlPath = props.entityId
278
- ? buildUrlCollectionPath(`${collectionPath}/${props.entityId}${props.selectedTab ? "/" + props.selectedTab : ""}#${SIDE_URL_HASH}`)
279
- : buildUrlCollectionPath(`${collectionPath}#${NEW_URL_HASH}`);
280
+ ? buildUrlCollectionPath(`${collectionPath}/${props.entityId}${props.selectedTab ? "/" + props.selectedTab : ""}${locationSearch}#${SIDE_URL_HASH}`)
281
+ : buildUrlCollectionPath(`${collectionPath}${locationSearch}#${NEW_URL_HASH}`);
280
282
 
281
283
  const resolvedPanelProps: EntitySidePanelProps<any> = {
282
284
  ...props,
@@ -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
  ;
@@ -374,6 +374,15 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
374
374
  */
375
375
  defaultViewMode?: ViewMode;
376
376
 
377
+ /**
378
+ * Which view modes are available for this collection.
379
+ * Possible values: "table", "cards", "kanban".
380
+ * Defaults to all three: ["table", "cards", "kanban"].
381
+ * Note: "kanban" will only be available if the collection has at least
382
+ * one string property with enumValues defined, regardless of this setting.
383
+ */
384
+ enabledViews?: ViewMode[];
385
+
377
386
  /**
378
387
  * Configuration for Kanban board view mode.
379
388
  * When set, the Kanban view mode becomes available.
@@ -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
 
@@ -11,8 +11,8 @@ import { sortProperties } from "./collections";
11
11
  import { isPropertyBuilder } from "./entities";
12
12
 
13
13
  function applyModifyFunction(modifyCollection: ((props: ModifyCollectionProps) => (EntityCollection | void)) | undefined,
14
- collection: EntityCollection,
15
- parentPaths: string[]) {
14
+ collection: EntityCollection,
15
+ parentPaths: string[]) {
16
16
  if (modifyCollection) {
17
17
  const modified = modifyCollection({
18
18
  collection,
@@ -34,9 +34,9 @@ function applyModifyFunction(modifyCollection: ((props: ModifyCollectionProps) =
34
34
  *
35
35
  */
36
36
  export function joinCollectionLists(targetCollections: EntityCollection[],
37
- sourceCollections: EntityCollection[] | undefined,
38
- parentPaths: string[] = [],
39
- modifyCollection?: (props: ModifyCollectionProps) => EntityCollection | void): EntityCollection[] {
37
+ sourceCollections: EntityCollection[] | undefined,
38
+ parentPaths: string[] = [],
39
+ modifyCollection?: (props: ModifyCollectionProps) => EntityCollection | void): EntityCollection[] {
40
40
 
41
41
  // merge collections that are in both lists
42
42
  const updatedCollections = (sourceCollections ?? [])
@@ -73,9 +73,9 @@ export function joinCollectionLists(targetCollections: EntityCollection[],
73
73
  *
74
74
  */
75
75
  export function mergeCollection(target: EntityCollection,
76
- source: EntityCollection,
77
- parentPaths: string[] = [],
78
- modifyCollection?: (props: ModifyCollectionProps) => EntityCollection | void
76
+ source: EntityCollection,
77
+ parentPaths: string[] = [],
78
+ modifyCollection?: (props: ModifyCollectionProps) => EntityCollection | void
79
79
  ): EntityCollection {
80
80
 
81
81
  const subcollectionsMerged = joinCollectionLists(
@@ -125,6 +125,8 @@ export function mergeCollection(target: EntityCollection,
125
125
  }
126
126
 
127
127
  function mergePropertyOrBuilder(target: PropertyOrBuilder, source: PropertyOrBuilder): PropertyOrBuilder {
128
+ if (!source) return target;
129
+ if (!target) return source;
128
130
  if (isPropertyBuilder(source)) {
129
131
  return source;
130
132
  } else if (isPropertyBuilder(target)) {
@@ -1,4 +1,4 @@
1
- import { AuthController, EntityCollection, PropertyConfig, ResolvedEntityCollection } from "../types";
1
+ import { AuthController, EntityCollection, Property, PropertyConfig, ResolvedEntityCollection } from "../types";
2
2
  import { isReferenceProperty } from "./property_utils";
3
3
  import { isPropertyBuilder } from "./entities";
4
4
  import { getFieldConfig } from "../core";
@@ -33,7 +33,7 @@ export function getEntityTitlePropertyKey<M extends Record<string, any>>(collect
33
33
  for (const key in collection.properties) {
34
34
  const property = collection.properties[key];
35
35
  if (!isPropertyBuilder(property)) {
36
- const field = getFieldConfig(property, propertyConfigs);
36
+ const field = getFieldConfig(property as Property, propertyConfigs);
37
37
  if (field?.key === "text_field") {
38
38
  return key;
39
39
  }
@@ -18,7 +18,7 @@ export function isReferenceProperty(
18
18
  authController: AuthController,
19
19
  propertyOrBuilder: PropertyOrBuilder,
20
20
  fields: Record<string, PropertyConfig>) {
21
- const resolvedProperty = resolveProperty({
21
+ const resolvedProperty: ResolvedProperty<any> | null = resolveProperty({
22
22
  propertyKey: "ignore", // TODO
23
23
  propertyOrBuilder,
24
24
  propertyConfigs: fields,
@@ -13,6 +13,7 @@ import {
13
13
  Properties,
14
14
  PropertiesOrBuilders,
15
15
  Property,
16
+ PropertyBuilder,
16
17
  PropertyConfig,
17
18
  PropertyOrBuilder,
18
19
  ResolvedArrayProperty,
@@ -62,6 +63,7 @@ export const resolveCollection = <M extends Record<string, any>,>
62
63
  const usedPreviousValues = previousValues ?? values ?? defaultValues;
63
64
 
64
65
  const resolvedProperties = Object.entries(collection.properties)
66
+ .filter(([, propertyOrBuilder]) => propertyOrBuilder != null)
65
67
  .map(([key, propertyOrBuilder]) => {
66
68
  const childResolvedProperty = resolveProperty({
67
69
  propertyKey: key,
@@ -108,7 +110,7 @@ export function resolveProperty<T extends CMSType = CMSType, M extends Record<st
108
110
  ...props
109
111
  }: {
110
112
  propertyKey?: string,
111
- propertyOrBuilder: PropertyOrBuilder<T, M> | ResolvedProperty<T>,
113
+ propertyOrBuilder: PropertyOrBuilder<T, M> | ResolvedProperty<T> | PropertyOrBuilder | Property | ResolvedProperty | undefined,
112
114
  values?: Partial<M>,
113
115
  previousValues?: Partial<M>,
114
116
  path?: string,
@@ -120,7 +122,7 @@ export function resolveProperty<T extends CMSType = CMSType, M extends Record<st
120
122
  authController: AuthController;
121
123
  }): ResolvedProperty<T> | null {
122
124
 
123
- if (typeof propertyOrBuilder === "object" && "resolved" in propertyOrBuilder) {
125
+ if (propertyOrBuilder !== null && typeof propertyOrBuilder === "object" && "resolved" in propertyOrBuilder) {
124
126
  return propertyOrBuilder as ResolvedProperty<T>;
125
127
  }
126
128
 
@@ -134,7 +136,7 @@ export function resolveProperty<T extends CMSType = CMSType, M extends Record<st
134
136
  throw Error("Trying to resolve a property builder without specifying the entity path");
135
137
 
136
138
  const usedPropertyValue = props.propertyKey ? getIn(props.values, props.propertyKey) : undefined;
137
- const result: Property<T> | null = propertyOrBuilder({
139
+ const result: Property<T> | null = (propertyOrBuilder as PropertyBuilder<T, M>)({
138
140
  ...props,
139
141
  path,
140
142
  propertyValue: usedPropertyValue,