@firecms/core 3.3.0-canary.040c21c → 3.3.0-canary.1e1cce9

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 (37) hide show
  1. package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
  2. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  3. package/dist/components/EntityPreview.d.ts +3 -1
  4. package/dist/index.es.js +408 -297
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +408 -297
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/types/collections.d.ts +17 -0
  9. package/dist/types/translations.d.ts +11 -3
  10. package/dist/util/entities.d.ts +1 -0
  11. package/package.json +5 -5
  12. package/src/app/Scaffold.tsx +13 -1
  13. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +2 -1
  14. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  15. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  16. package/src/components/EntityCollectionView/EntityCollectionView.tsx +5 -2
  17. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -1
  18. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  19. package/src/components/EntityPreview.tsx +41 -40
  20. package/src/components/HomePage/NavigationCardBinding.tsx +6 -3
  21. package/src/components/ReferenceWidget.tsx +1 -1
  22. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  23. package/src/components/common/useDataSourceTableController.tsx +41 -4
  24. package/src/core/DefaultDrawer.tsx +1 -1
  25. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  26. package/src/hooks/useBuildNavigationController.tsx +5 -1
  27. package/src/locales/de.ts +13 -5
  28. package/src/locales/en.ts +13 -5
  29. package/src/locales/es.ts +13 -5
  30. package/src/locales/fr.ts +13 -5
  31. package/src/locales/hi.ts +13 -5
  32. package/src/locales/it.ts +13 -5
  33. package/src/locales/pt.ts +13 -5
  34. package/src/types/collections.ts +18 -0
  35. package/src/types/translations.ts +11 -3
  36. package/src/util/entities.ts +11 -0
  37. package/src/util/resolutions.ts +3 -0
@@ -232,6 +232,21 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
232
232
  * e.g. `initialFilter: { related_user: ["==", new EntityReference("sdc43dsw2", "users")] }`
233
233
  */
234
234
  initialFilter?: FilterValues<Extract<keyof M, string>>;
235
+ /**
236
+ * Array of property keys that are allowed for filtering. Allowed filters will be displayed in the collection view, table row headers
237
+ * and will be used to filter data.
238
+ *
239
+ * If not specified, all properties that are filterable will be allowed.
240
+ *
241
+ * If you specify this prop, the filters will be displayed in the collection view if they are filterable.
242
+ *
243
+ * If you set it as an empty array, no filters will be displayed.
244
+ *
245
+ * e.g. `allowedFilters: ["name", "category"]`
246
+ *
247
+ * e.g. `allowedFilters: []`
248
+ */
249
+ allowedFilters?: (keyof M)[];
235
250
  /**
236
251
  * Default sort applied to this collection.
237
252
  * When setting this prop, entities will have a default order
@@ -621,6 +636,8 @@ export type EntityTableController<M extends Record<string, any> = any> = {
621
636
  dataLoadingError?: Error;
622
637
  filterValues?: FilterValues<Extract<keyof M, string>>;
623
638
  setFilterValues?: (filterValues: FilterValues<Extract<keyof M, string>>) => void;
639
+ allowedFilters?: (keyof M)[];
640
+ forcedFilters?: (keyof M)[];
624
641
  sortBy?: [Extract<keyof M, string>, "asc" | "desc"];
625
642
  setSortBy?: (sortBy?: [Extract<keyof M, string>, "asc" | "desc"]) => void;
626
643
  searchString?: string;
@@ -412,8 +412,8 @@ export interface FireCMSTranslations {
412
412
  cms_users: string;
413
413
  roles_menu: string;
414
414
  project_settings: string;
415
- firestore_explorer: string;
416
- explore_your_firestore_data: string;
415
+ firestore_manager: string;
416
+ manage_your_firestore_data: string;
417
417
  build_admin_panel_in_minutes: string;
418
418
  go_live_instantly: string;
419
419
  create_production_ready_back_offices: string;
@@ -481,10 +481,18 @@ export interface FireCMSTranslations {
481
481
  auto_setup_collections_button: string;
482
482
  auto_setup_collections_title: string;
483
483
  auto_setup_collections_desc: string;
484
- this_can_take_a_minute: string;
484
+ setting_up_collections: string;
485
+ setting_up_collection: string;
485
486
  no_collections_found_to_setup: string;
486
487
  collections_have_been_setup: string;
487
488
  error_setting_up_collections: string;
489
+ setup_collections_title: string;
490
+ setup_collections_select_desc: string;
491
+ select_all: string;
492
+ deselect_all: string;
493
+ setup_collections_confirm: string;
494
+ collection_setup_success: string;
495
+ go_to_collection: string;
488
496
  add_your: string;
489
497
  database_collections: string;
490
498
  to_firecms: string;
@@ -25,3 +25,4 @@ export declare function sanitizeData<M extends Record<string, any>>(values: Enti
25
25
  export declare function getReferenceFrom<M extends Record<string, any>>(entity: Entity<M>): EntityReference;
26
26
  export declare function traverseValuesProperties<M extends Record<string, any>>(inputValues: Partial<EntityValues<M>>, properties: ResolvedProperties<M>, operation: (value: any, property: Property) => any): EntityValues<M> | undefined;
27
27
  export declare function traverseValueProperty(inputValue: any, property: Property, operation: (value: any, property: Property) => any): any;
28
+ export declare function isDataTypeFilterable(dataType: DataType, isPartOfArray?: boolean): boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/core",
3
3
  "type": "module",
4
- "version": "3.3.0-canary.040c21c",
4
+ "version": "3.3.0-canary.1e1cce9",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -53,8 +53,8 @@
53
53
  "@dnd-kit/core": "^6.3.1",
54
54
  "@dnd-kit/modifiers": "^9.0.0",
55
55
  "@dnd-kit/sortable": "^10.0.0",
56
- "@firecms/formex": "^3.3.0-canary.040c21c",
57
- "@firecms/ui": "^3.3.0-canary.040c21c",
56
+ "@firecms/formex": "^3.3.0-canary.1e1cce9",
57
+ "@firecms/ui": "^3.3.0-canary.1e1cce9",
58
58
  "@floating-ui/dom": "^1.7.4",
59
59
  "@radix-ui/react-portal": "^1.1.10",
60
60
  "@radix-ui/react-slot": "^1.2.4",
@@ -65,7 +65,7 @@
65
65
  "history": "^5.3.0",
66
66
  "i18next": "^23.16.4",
67
67
  "json-logic-js": "^2.0.5",
68
- "markdown-it": "^14.1.0",
68
+ "markdown-it": "^14.2.0",
69
69
  "markdown-it-ins": "^4.0.0",
70
70
  "markdown-it-mark": "^4.0.0",
71
71
  "markdown-it-task-lists": "^2.1.1",
@@ -123,7 +123,7 @@
123
123
  "eslint-plugin-react-compiler": "^19.1.0-rc.2",
124
124
  "jest": "^29.7.0",
125
125
  "jest-environment-jsdom": "^30.2.0",
126
- "npm-run-all": "^4.1.5",
126
+ "npm-run-all2": "^6.2.6",
127
127
  "react-router": "^6.30.2",
128
128
  "react-router-dom": "^6.30.2",
129
129
  "ts-jest": "^29.4.5",
@@ -70,7 +70,13 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
70
70
  const includeDrawer = drawerChildren.length > 0;
71
71
  const largeLayout = useLargeLayout();
72
72
 
73
- const [drawerOpen, setDrawerOpen] = React.useState(false);
73
+ const [drawerOpen, setDrawerOpen] = React.useState(() => {
74
+ try {
75
+ return localStorage.getItem("firecms_drawer_open") === "true";
76
+ } catch {
77
+ return false;
78
+ }
79
+ });
74
80
  const [onHover, setOnHover] = React.useState(false);
75
81
 
76
82
  const setOnHoverTrue = useCallback(() => setOnHover(true), []);
@@ -78,10 +84,16 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
78
84
 
79
85
  const handleDrawerOpen = useCallback(() => {
80
86
  setDrawerOpen(true);
87
+ try {
88
+ localStorage.setItem("firecms_drawer_open", "true");
89
+ } catch { /* ignore */ }
81
90
  }, []);
82
91
 
83
92
  const handleDrawerClose = useCallback(() => {
84
93
  setDrawerOpen(false);
94
+ try {
95
+ localStorage.setItem("firecms_drawer_open", "false");
96
+ } catch { /* ignore */ }
85
97
  }, []);
86
98
 
87
99
  const computedDrawerOpen: boolean = drawerOpen || Boolean(largeLayout && autoOpenDrawer && onHover);
@@ -245,7 +245,8 @@ export const EntityCollectionTable = function EntityCollectionTable<M extends Re
245
245
  const columnsResult: VirtualTableColumn[] = propertiesToColumns({
246
246
  properties,
247
247
  sortable,
248
- forceFilter,
248
+ forcedFilters: tableController.forcedFilters,
249
+ allowedFilters: tableController.allowedFilters,
249
250
  AdditionalHeaderWidget
250
251
  });
251
252
 
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { getTableCellAlignment, getTablePropertyColumnWidth } from "./internal/common";
3
3
  import { FilterValues, ResolvedProperties, ResolvedProperty } from "../../types";
4
4
  import { VirtualTableColumn } from "../VirtualTable";
5
- import { getIconForProperty, getResolvedPropertyInPath } from "../../util";
5
+ import { getIconForProperty, getResolvedPropertyInPath, isDataTypeFilterable } from "../../util";
6
6
  import { getColumnKeysForProperty } from "../common/useColumnsIds";
7
7
 
8
8
  export function buildIdColumn(largeLayout?: boolean): VirtualTableColumn {
@@ -20,7 +20,8 @@ export function buildIdColumn(largeLayout?: boolean): VirtualTableColumn {
20
20
  export interface PropertiesToColumnsParams<M extends Record<string, any>> {
21
21
  properties: ResolvedProperties<M>;
22
22
  sortable?: boolean;
23
- forceFilter?: FilterValues<keyof M extends string ? keyof M : never>;
23
+ forcedFilters?: (keyof M)[];
24
+ allowedFilters?: (keyof M)[];
24
25
  AdditionalHeaderWidget?: React.ComponentType<{
25
26
  property: ResolvedProperty,
26
27
  propertyKey: string,
@@ -28,8 +29,7 @@ export interface PropertiesToColumnsParams<M extends Record<string, any>> {
28
29
  }>;
29
30
  }
30
31
 
31
- export function propertiesToColumns<M extends Record<string, any>>({ properties, sortable, forceFilter, AdditionalHeaderWidget }: PropertiesToColumnsParams<M>): VirtualTableColumn[] {
32
- const disabledFilter = Boolean(forceFilter);
32
+ export function propertiesToColumns<M extends Record<string, any>>({ properties, sortable, forcedFilters, AdditionalHeaderWidget, allowedFilters }: PropertiesToColumnsParams<M>): VirtualTableColumn[] {
33
33
  return Object.entries<ResolvedProperty>(properties)
34
34
  .flatMap(([key, property]) => getColumnKeysForProperty(property, key))
35
35
  .map(({
@@ -39,14 +39,19 @@ export function propertiesToColumns<M extends Record<string, any>>({ properties,
39
39
  const property = getResolvedPropertyInPath(properties, key);
40
40
  if (!property)
41
41
  throw Error("Internal error: no property found in path " + key);
42
- const filterable = filterableProperty(property);
42
+
43
+ const filterable = property.dataType === 'array' ? isDataTypeFilterable(property.of?.dataType, true) : isDataTypeFilterable(property.dataType);
44
+ const isFilterForced = forcedFilters?.includes(key) ?? false;
45
+ const isFilterAllowed = allowedFilters ? allowedFilters.includes(key) : filterable;
46
+
47
+ const filterEnabled = filterable && isFilterAllowed && !isFilterForced;
43
48
  return {
44
49
  key: key as string,
45
50
  align: getTableCellAlignment(property),
46
51
  icon: getIconForProperty(property, "small"),
47
52
  title: property.name ?? key as string,
48
53
  sortable: sortable,
49
- filter: !disabledFilter && filterable,
54
+ filter: filterEnabled,
50
55
  width: getTablePropertyColumnWidth(property),
51
56
  resizable: true,
52
57
  custom: {
@@ -59,16 +64,3 @@ export function propertiesToColumns<M extends Record<string, any>>({ properties,
59
64
  } satisfies VirtualTableColumn;
60
65
  });
61
66
  }
62
-
63
- function filterableProperty(property: ResolvedProperty, partOfArray = false): boolean {
64
- if (partOfArray) {
65
- return ["string", "number", "date", "reference"].includes(property.dataType);
66
- }
67
- if (property.dataType === "array") {
68
- if (property.of)
69
- return filterableProperty(property.of, true);
70
- else
71
- return false;
72
- }
73
- return ["string", "number", "boolean", "date", "reference", "array"].includes(property.dataType);
74
- }
@@ -70,7 +70,7 @@ export const TableReferenceFieldInternal = React.memo(
70
70
  }, [updateValue]);
71
71
 
72
72
  const onMultipleEntitiesSelected = useCallback((entities: Entity<any>[]) => {
73
- updateValue(entities.map((e) => getReferenceFrom(e)));
73
+ updateValue(entities.filter(Boolean).map((e) => getReferenceFrom(e)));
74
74
  }, [updateValue]);
75
75
 
76
76
  const selectedEntityIds = internalValue
@@ -178,7 +178,9 @@ export const EntityCollectionView = React.memo(
178
178
 
179
179
  const collection = useMemo(() => {
180
180
  const userOverride = userConfigPersistence?.getCollectionConfig<M>(fullPath);
181
- return (userOverride ? mergeDeep(collectionProp, userOverride) : collectionProp) as EntityCollection<M>;
181
+ if (!userOverride) return collectionProp;
182
+ const { properties, ...rest } = userOverride;
183
+ return mergeDeep(collectionProp, rest) as EntityCollection<M>;
182
184
  }, [collectionProp, fullPath, userConfigPersistence?.getCollectionConfig]);
183
185
 
184
186
  const openEntityMode = collection?.openEntityMode ?? DEFAULT_ENTITY_OPEN_MODE;
@@ -507,7 +509,8 @@ export const EntityCollectionView = React.memo(
507
509
  path: fullPath,
508
510
  propertyConfigs: customizationController.propertyConfigs,
509
511
  authController,
510
- }), [collection, fullPath]);
512
+ userConfigPersistence,
513
+ }), [collection, fullPath, userConfigPersistence]);
511
514
 
512
515
  // Check if Kanban view is possible (collection has at least one string enum property)
513
516
  const hasEnumProperty = useMemo(() => {
@@ -66,8 +66,10 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
66
66
  collectionEntitiesCount
67
67
  };
68
68
 
69
+ const hasAnyAllowedFilters = !tableController.allowedFilters || tableController.allowedFilters.length > 0;
70
+
69
71
  // Filters button
70
- const filtersButton = resolvedProperties && tableController.setFilterValues && (
72
+ const filtersButton = resolvedProperties && tableController.setFilterValues && hasAnyAllowedFilters && (
71
73
  <Tooltip title={t("filters")}
72
74
  key={"filters_tooltip"}>
73
75
  <Badge
@@ -131,6 +133,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
131
133
  filterValues={tableController.filterValues}
132
134
  setFilterValues={(filterValues) => tableController.setFilterValues?.(filterValues ?? {})}
133
135
  forceFilter={collection.forceFilter}
136
+ allowedFilters={tableController.allowedFilters?.map(key => key.toString())}
134
137
  />
135
138
  )}
136
139
  </>
@@ -12,7 +12,6 @@ import {
12
12
  DialogActions,
13
13
  DialogContent,
14
14
  DialogTitle,
15
- FilterListIcon,
16
15
  Typography
17
16
  } from "@firecms/ui";
18
17
  import { useTranslation } from "../../hooks/useTranslation";
@@ -30,6 +29,7 @@ export interface FiltersDialogProps {
30
29
  filterValues: FilterValues<any> | undefined;
31
30
  setFilterValues: (filterValues?: FilterValues<any>) => void;
32
31
  forceFilter?: FilterValues<any>;
32
+ allowedFilters?: string[];
33
33
  }
34
34
 
35
35
  /**
@@ -42,7 +42,8 @@ export function FiltersDialog({
42
42
  properties,
43
43
  filterValues,
44
44
  setFilterValues,
45
- forceFilter
45
+ forceFilter,
46
+ allowedFilters
46
47
  }: FiltersDialogProps) {
47
48
  const { t } = useTranslation();
48
49
 
@@ -59,18 +60,18 @@ export function FiltersDialog({
59
60
  }
60
61
  }, [open, filterValues]);
61
62
 
62
- // Get list of filterable properties
63
- const filterableProperties = useMemo(() => {
64
- return Object.entries(properties).filter(([key, property]) => {
65
- if (!property) return false;
66
- // Force filter properties should not be editable
67
- if (forceFilter && key in forceFilter) return false;
68
- // Check if property type is filterable
69
- const baseProperty = property.dataType === "array" ? property.of : property;
70
- if (!baseProperty) return false;
71
- return ["string", "number", "boolean", "date", "reference"].includes(baseProperty.dataType);
63
+ // Get list of editable filter properties
64
+ const editableFilterProperties = useMemo(() => {
65
+ return Object.entries(properties).filter(([key]) => {
66
+ const isFilterAllowed = !allowedFilters || allowedFilters.includes(key);
67
+
68
+ const isFilterForced = Boolean(forceFilter && Object.keys(forceFilter).includes(key));
69
+
70
+ return isFilterAllowed && !isFilterForced;
72
71
  });
73
- }, [properties, forceFilter]);
72
+ }, [properties, allowedFilters, forceFilter]);
73
+
74
+ const hasEditableFilterProperties = editableFilterProperties.length > 0;
74
75
 
75
76
  const handleFilterChange = useCallback((propertyKey: string, value?: [VirtualTableWhereFilterOp, any]) => {
76
77
  setLocalFilters(prev => {
@@ -103,7 +104,13 @@ export function FiltersDialog({
103
104
 
104
105
  // Check if any reference field's dialog is currently open (should hide this dialog)
105
106
  const isAnyFieldHidden = Object.values(hiddenFields).some(hidden => hidden);
106
- const activeFilterCount = Object.keys(localFilters).length;
107
+
108
+ const getActiveFilterCount = () => {
109
+ const editableLocalFilters = Object.keys(localFilters).filter((key) => !forceFilter || !(key in forceFilter));
110
+ return editableLocalFilters.length;
111
+ }
112
+
113
+ const activeFilterCount = getActiveFilterCount();
107
114
 
108
115
  const renderFilterField = useCallback((propertyKey: string, property: ResolvedProperty) => {
109
116
  const isArray = property.dataType === "array";
@@ -185,14 +192,14 @@ export function FiltersDialog({
185
192
  </DialogTitle>
186
193
 
187
194
  <DialogContent >
188
- {filterableProperties.length === 0 ? (
195
+ {!hasEditableFilterProperties ? (
189
196
  <Typography color="secondary" className="py-8 text-center">
190
197
  {t("no_filterable_properties")}
191
198
  </Typography>
192
199
  ) : (
193
200
  <table className="w-full border-collapse">
194
201
  <tbody>
195
- {filterableProperties.map(([propertyKey, property], index) => {
202
+ {editableFilterProperties.map(([propertyKey, property], index) => {
196
203
  const hasFilter = propertyKey in localFilters;
197
204
 
198
205
  return (
@@ -234,18 +241,22 @@ export function FiltersDialog({
234
241
  {t("clear")}
235
242
  </Button>
236
243
  <div className="flex-grow" />
237
- <Button
238
- variant="text"
239
- onClick={() => onOpenChange(false)}
240
- >
241
- {t("cancel")}
242
- </Button>
243
- <Button
244
- variant="filled"
245
- onClick={handleApply}
246
- >
247
- {t("apply_filters")}
248
- </Button>
244
+ {hasEditableFilterProperties && (
245
+ <>
246
+ <Button
247
+ variant="text"
248
+ onClick={() => onOpenChange(false)}
249
+ >
250
+ {t("cancel")}
251
+ </Button>
252
+ <Button
253
+ variant="filled"
254
+ onClick={handleApply}
255
+ >
256
+ {t("apply_filters")}
257
+ </Button>
258
+ </>
259
+ )}
249
260
  </DialogActions>
250
261
  </Dialog>
251
262
  );
@@ -200,43 +200,44 @@ export type EntityPreviewContainerProps = {
200
200
  onClick?: (e: React.SyntheticEvent) => void;
201
201
  };
202
202
 
203
- export const EntityPreviewContainer = React.forwardRef<HTMLDivElement, EntityPreviewContainerProps>(({
204
- children,
205
- hover,
206
- onClick,
207
- size = "medium",
208
- style,
209
- className,
210
- fullwidth = true,
211
- ...props
212
- }, ref) => {
213
- return <div
214
- ref={ref}
215
- style={{
216
- ...style,
217
- // @ts-ignore
218
- tabindex: 0
219
- }}
220
- className={cls(
221
- "bg-white dark:bg-surface-900",
222
- "min-h-[44px]",
223
- fullwidth ? "w-full" : "",
224
- "items-center",
225
- hover ? "hover:bg-surface-accent-50 dark:hover:bg-surface-800 group-hover:bg-surface-accent-50 dark:group-hover:bg-surface-800" : "",
226
- size === "small" ? "p-1" : "px-2 py-1",
227
- "flex border rounded-lg",
228
- onClick ? "cursor-pointer" : "",
229
- defaultBorderMixin,
230
- className)}
231
- onClick={(event) => {
232
- if (onClick) {
233
- event.preventDefault();
234
- onClick(event);
235
- }
236
- }}
237
- {...props}>
238
- {children}
239
- </div>;
240
- });
241
-
242
- EntityPreviewContainer.displayName = "EntityPreviewContainer";
203
+ export function EntityPreviewContainer({
204
+ children,
205
+ hover,
206
+ onClick,
207
+ size = "medium",
208
+ style,
209
+ className,
210
+ fullwidth = true,
211
+ ref
212
+ }: EntityPreviewContainerProps & { ref?: React.Ref<HTMLDivElement> }) {
213
+ const divClassName = cls(
214
+ "bg-white dark:bg-surface-900",
215
+ "min-h-[44px]",
216
+ fullwidth ? "w-full" : "",
217
+ "items-center",
218
+ hover ? "hover:bg-surface-accent-50 dark:hover:bg-surface-800 group-hover:bg-surface-accent-50 dark:group-hover:bg-surface-800" : "",
219
+ size === "small" ? "p-1" : "px-2 py-1",
220
+ "flex border rounded-lg",
221
+ onClick ? "cursor-pointer" : "",
222
+ defaultBorderMixin,
223
+ className
224
+ );
225
+
226
+ const handleClick = onClick
227
+ ? (event: React.MouseEvent<HTMLDivElement>) => {
228
+ event.preventDefault();
229
+ onClick(event);
230
+ }
231
+ : undefined;
232
+
233
+ const divProps: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> } = {
234
+ ref,
235
+ tabIndex: 0,
236
+ style,
237
+ className: divClassName,
238
+ onClick: handleClick,
239
+ };
240
+
241
+ return <div {...divProps}>{children}</div>;
242
+ }
243
+
@@ -1,6 +1,6 @@
1
1
  import { useNavigate } from "react-router-dom";
2
2
 
3
- import { useCustomizationController, useFireCMSContext } from "../../hooks";
3
+ import { useCustomizationController, useFireCMSContext, useTranslation } from "../../hooks";
4
4
  import { NavigationEntry, PluginHomePageActionsProps } from "../../types";
5
5
  import { IconForView } from "../../util";
6
6
  import { useUserConfigurationPersistence } from "../../hooks/useUserConfigurationPersistence";
@@ -38,6 +38,7 @@ export function NavigationCardBinding({
38
38
  }) {
39
39
 
40
40
  const userConfigurationPersistence = useUserConfigurationPersistence();
41
+ const { t } = useTranslation();
41
42
  const collectionIcon = <IconForView collectionOrView={collection ?? view}/>;
42
43
 
43
44
  const navigate = useNavigate();
@@ -92,15 +93,17 @@ export function NavigationCardBinding({
92
93
  {actionsArray}
93
94
  </>
94
95
 
96
+ const translatedName = t(name);
97
+
95
98
  if (type === "admin") {
96
99
  return <SmallNavigationCard icon={collectionIcon}
97
- name={name}
100
+ name={translatedName}
98
101
  url={url}/>;
99
102
  }
100
103
 
101
104
  return <NavigationCard
102
105
  icon={collectionIcon}
103
- name={name}
106
+ name={translatedName}
104
107
  description={description}
105
108
  actions={actions}
106
109
  onClick={() => {
@@ -73,7 +73,7 @@ export function ReferenceWidget<M extends Record<string, any>>({
73
73
  if (disabled)
74
74
  return;
75
75
  if (onMultipleReferenceSelected) {
76
- const references = entities ? entities.map(e => getReferenceFrom(e)) : null;
76
+ const references = entities ? entities.filter(Boolean).map(e => getReferenceFrom(e)) : null;
77
77
  onMultipleReferenceSelected({
78
78
  references,
79
79
  entities
@@ -113,11 +113,11 @@ export function ReferenceFilterField({
113
113
  }, [path]);
114
114
 
115
115
  const onSingleEntitySelected = (entity: Entity<any>) => {
116
- updateFilter(operation, getReferenceFrom(entity));
116
+ if (entity) updateFilter(operation, getReferenceFrom(entity));
117
117
  };
118
118
 
119
119
  const onMultipleEntitiesSelected = (entities: Entity<any>[]) => {
120
- updateFilter(operation, entities.map(e => getReferenceFrom(e)));
120
+ updateFilter(operation, entities.filter(Boolean).map(e => getReferenceFrom(e)));
121
121
  };
122
122
 
123
123
  const multiple = multipleSelectOperations.includes(operation);
@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
4
4
  import { useDataSource, useFireCMSContext, useNavigationController } from "../../hooks";
5
5
  import { useDataOrder } from "../../hooks/data/useDataOrder";
6
6
  import {
7
+ DataType,
7
8
  Entity,
8
9
  EntityCollection,
9
10
  EntityReference,
@@ -16,6 +17,7 @@ import {
16
17
  } from "../../types";
17
18
  import { useDebouncedData } from "./useDebouncedData";
18
19
  import { ScrollRestorationController } from "./useScrollRestoration";
20
+ import { isDataTypeFilterable } from "../../util";
19
21
 
20
22
  const DEFAULT_PAGE_SIZE = 50;
21
23
 
@@ -135,8 +137,41 @@ export function useDataSourceTableController<M extends Record<string, any> = any
135
137
  filterValues: initialFilterUrl,
136
138
  sortBy: initialSortUrl,
137
139
  } = parseFilterAndSort(location.search);
140
+
141
+ const availableFilterKeys = collection.allowedFilters ?? Object.keys(collection.properties);
142
+ const forcedFilterKeys = collection.forceFilter ? Object.keys(collection.forceFilter) : [];
138
143
 
139
- const [filterValues, setFilterValues] = React.useState<FilterValues<Extract<keyof M, string>> | undefined>(forceFilter ?? (updateUrl ? initialFilterUrl : undefined) ?? initialFilter ?? undefined);
144
+ const allowedFilterKeys = useMemo(() => {
145
+ const availableKeys = availableFilterKeys.filter((key) => {
146
+ const property = collection.properties[key];
147
+
148
+ if (!property) return false;
149
+
150
+ if (typeof property === "function") return false;
151
+
152
+ const dataType = property.dataType;
153
+ const filterable = dataType === "array"
154
+ ? isDataTypeFilterable((property as { of?: { dataType: DataType } }).of?.dataType as DataType, true)
155
+ : isDataTypeFilterable(dataType);
156
+
157
+ return filterable;
158
+ });
159
+
160
+ const forcedKeys = forcedFilterKeys.filter((key) => !availableKeys.includes(key));
161
+
162
+ return [...availableKeys, ...forcedKeys];
163
+
164
+ }, [collection.properties, availableFilterKeys, forcedFilterKeys]);
165
+
166
+ const removeUnallowedFilters = useCallback((filters?: FilterValues<Extract<keyof M, string>>) => {
167
+ if (!filters) return;
168
+
169
+ return Object.fromEntries(Object.entries(filters).filter(([key]) => allowedFilterKeys.includes(key as keyof M))) as FilterValues<Extract<keyof M, string>>;
170
+ }, [allowedFilterKeys]);
171
+
172
+ const initFilters = forceFilter ?? (updateUrl ? initialFilterUrl : undefined) ?? initialFilter ?? undefined
173
+
174
+ const [filterValues, setFilterValues] = React.useState<FilterValues<Extract<keyof M, string>> | undefined>(removeUnallowedFilters(initFilters));
140
175
  const [sortBy, setSortBy] = React.useState<[Extract<keyof M, string>, "asc" | "desc"] | undefined>((updateUrl ? initialSortUrl : undefined) ?? initialSortInternal);
141
176
 
142
177
  useUpdateUrl(filterValues, sortBy, searchString, updateUrl);
@@ -168,7 +203,7 @@ export function useDataSourceTableController<M extends Record<string, any> = any
168
203
  const [dataLoadingError, setDataLoadingError] = useState<Error | undefined>();
169
204
  const [noMoreToLoad, setNoMoreToLoad] = useState<boolean>(false);
170
205
 
171
- const clearFilter = useCallback(() => setFilterValues(forceFilter ?? undefined), [forceFilter]);
206
+ const clearFilter = useCallback(() => setFilterValues(removeUnallowedFilters(forceFilter)), [forceFilter, removeUnallowedFilters]);
172
207
 
173
208
  const updateFilterValues = useCallback((updatedFilter: FilterValues<Extract<keyof M, string>> | undefined) => {
174
209
  if (forceFilter) {
@@ -178,9 +213,9 @@ export function useDataSourceTableController<M extends Record<string, any> = any
178
213
  if (updatedFilter && Object.keys(updatedFilter).length === 0) {
179
214
  setFilterValues(undefined);
180
215
  } else {
181
- setFilterValues(updatedFilter);
216
+ setFilterValues(removeUnallowedFilters(updatedFilter));
182
217
  }
183
- }, [forceFilter]);
218
+ }, [forceFilter, removeUnallowedFilters]);
184
219
 
185
220
  useEffect(() => {
186
221
 
@@ -268,6 +303,8 @@ export function useDataSourceTableController<M extends Record<string, any> = any
268
303
  dataLoadingError,
269
304
  filterValues,
270
305
  setFilterValues: updateFilterValues,
306
+ allowedFilters: allowedFilterKeys,
307
+ forcedFilters: forcedFilterKeys,
271
308
  sortBy,
272
309
  setSortBy,
273
310
  searchString,
@@ -128,7 +128,7 @@ export function DefaultDrawer({
128
128
  }}
129
129
  key={entry.id}>
130
130
  {<IconForView collectionOrView={entry.view} />}
131
- {t(entry.name as any)}
131
+ {t(entry.name)}
132
132
  </MenuItem>)}
133
133
 
134
134
  </Menu>}
@@ -58,7 +58,7 @@ export function ArrayOfReferencesFieldBinding({
58
58
  }
59
59
 
60
60
  const onMultipleEntitiesSelected = useCallback((entities: Entity<any>[]) => {
61
- setValue(entities.map(e => getReferenceFrom(e)));
61
+ setValue(entities.filter(Boolean).map(e => getReferenceFrom(e)));
62
62
  }, [setValue]);
63
63
 
64
64
  const referenceDialogController = useReferenceDialog({