@firecms/core 3.3.0-canary.d3242eb → 3.4.0-canary.63041d0

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 (110) hide show
  1. package/dist/app/useApp.d.ts +1 -0
  2. package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
  3. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  4. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +7 -0
  5. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  6. package/dist/components/EntityPreview.d.ts +3 -1
  7. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  8. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  9. package/dist/components/common/table_url_params.d.ts +24 -0
  10. package/dist/components/common/useDataSourceTableController.d.ts +7 -1
  11. package/dist/components/common/useTableSearchHelper.d.ts +7 -1
  12. package/dist/core/DefaultDrawer.d.ts +10 -3
  13. package/dist/core/EntityEditView.d.ts +7 -1
  14. package/dist/core/field_configs.d.ts +1 -1
  15. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
  16. package/dist/form/index.d.ts +1 -0
  17. package/dist/hooks/data/save.d.ts +1 -1
  18. package/dist/hooks/data/useEntityFetch.d.ts +6 -1
  19. package/dist/index.es.js +2960 -1338
  20. package/dist/index.es.js.map +1 -1
  21. package/dist/index.umd.js +2958 -1336
  22. package/dist/index.umd.js.map +1 -1
  23. package/dist/locales/pl.d.ts +2 -0
  24. package/dist/preview/index.d.ts +1 -0
  25. package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
  26. package/dist/types/collections.d.ts +24 -0
  27. package/dist/types/datasource.d.ts +42 -0
  28. package/dist/types/properties.d.ts +17 -1
  29. package/dist/types/side_entity_controller.d.ts +6 -0
  30. package/dist/types/translations.d.ts +17 -3
  31. package/dist/util/__tests__/collections.test.d.ts +1 -0
  32. package/dist/util/__tests__/urls.test.d.ts +1 -0
  33. package/dist/util/collections.d.ts +1 -1
  34. package/dist/util/entities.d.ts +1 -0
  35. package/dist/util/geopoint.d.ts +22 -0
  36. package/dist/util/index.d.ts +2 -0
  37. package/dist/util/navigation_blocking.d.ts +22 -0
  38. package/dist/util/navigation_from_path.d.ts +17 -0
  39. package/dist/util/navigation_utils.d.ts +23 -1
  40. package/dist/util/urls.d.ts +22 -0
  41. package/package.json +16 -9
  42. package/src/app/Scaffold.tsx +47 -45
  43. package/src/app/useApp.tsx +1 -0
  44. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
  45. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  46. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  47. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  48. package/src/components/EntityCollectionView/EntityCollectionView.tsx +24 -3
  49. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -1
  50. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  51. package/src/components/EntityPreview.tsx +41 -40
  52. package/src/components/HomePage/NavigationCardBinding.tsx +6 -3
  53. package/src/components/ReferenceWidget.tsx +1 -1
  54. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  55. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  56. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  57. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  58. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  59. package/src/components/common/table_url_params.ts +172 -0
  60. package/src/components/common/useDataSourceTableController.tsx +98 -159
  61. package/src/components/common/useTableSearchHelper.ts +21 -2
  62. package/src/contexts/SnackbarProvider.tsx +14 -1
  63. package/src/core/DefaultDrawer.tsx +151 -65
  64. package/src/core/DrawerNavigationGroup.tsx +27 -29
  65. package/src/core/DrawerNavigationItem.tsx +10 -12
  66. package/src/core/EntityEditView.tsx +75 -32
  67. package/src/core/EntitySidePanel.tsx +2 -0
  68. package/src/core/field_configs.tsx +15 -0
  69. package/src/form/EntityForm.tsx +1 -1
  70. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  71. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  72. package/src/form/index.tsx +1 -0
  73. package/src/hooks/data/save.ts +2 -0
  74. package/src/hooks/data/useEntityFetch.tsx +9 -0
  75. package/src/hooks/useBuildNavigationController.tsx +5 -1
  76. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  77. package/src/i18n/FireCMSi18nProvider.tsx +2 -0
  78. package/src/internal/useBuildDataSource.ts +11 -0
  79. package/src/internal/useBuildSideEntityController.tsx +8 -1
  80. package/src/locales/de.ts +18 -5
  81. package/src/locales/en.ts +18 -5
  82. package/src/locales/es.ts +18 -5
  83. package/src/locales/fr.ts +18 -5
  84. package/src/locales/hi.ts +18 -5
  85. package/src/locales/it.ts +18 -5
  86. package/src/locales/pl.ts +735 -0
  87. package/src/locales/pt.ts +18 -5
  88. package/src/preview/PropertyPreview.tsx +12 -0
  89. package/src/preview/components/StorageThumbnail.tsx +24 -2
  90. package/src/preview/index.ts +1 -0
  91. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  92. package/src/routes/FireCMSRoute.tsx +47 -25
  93. package/src/types/collections.ts +26 -0
  94. package/src/types/datasource.ts +42 -0
  95. package/src/types/properties.ts +17 -0
  96. package/src/types/side_entity_controller.tsx +6 -0
  97. package/src/types/translations.ts +17 -3
  98. package/src/util/__tests__/collections.test.ts +32 -0
  99. package/src/util/__tests__/urls.test.ts +131 -0
  100. package/src/util/collections.ts +1 -1
  101. package/src/util/entities.ts +13 -0
  102. package/src/util/geopoint.ts +81 -0
  103. package/src/util/index.ts +2 -0
  104. package/src/util/navigation_blocking.ts +45 -0
  105. package/src/util/navigation_from_path.ts +41 -6
  106. package/src/util/navigation_utils.ts +40 -2
  107. package/src/util/parent_references_from_path.ts +4 -2
  108. package/src/util/previews.ts +8 -1
  109. package/src/util/resolutions.ts +11 -0
  110. package/src/util/urls.ts +52 -0
@@ -10,6 +10,7 @@ export type AppState = {
10
10
  drawerOpen: boolean,
11
11
  openDrawer: () => void,
12
12
  closeDrawer: () => void,
13
+ closeHover?: () => void,
13
14
  autoOpenDrawer?: boolean,
14
15
  logo?: string
15
16
  }
@@ -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
 
@@ -353,6 +354,7 @@ export const EntityCollectionTable = function EntityCollectionTable<M extends Re
353
354
  onTextSearch={textSearchEnabled ? onTextSearch : undefined}
354
355
  textSearchLoading={textSearchLoading}
355
356
  onTextSearchClick={textSearchEnabled ? onTextSearchClick : undefined}
357
+ initialSearchString={tableController.searchString}
356
358
  title={title}
357
359
  actionsStart={actionsStart}
358
360
  actions={actions}
@@ -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
@@ -20,6 +20,11 @@ interface CollectionTableToolbarProps {
20
20
  onTextSearchClick?: () => void;
21
21
  onTextSearch?: (searchString?: string) => void;
22
22
  textSearchLoading?: boolean;
23
+ /**
24
+ * Search term the search bar should start with, typically the one restored from the
25
+ * URL query params by the table controller.
26
+ */
27
+ initialSearchString?: string;
23
28
  }
24
29
 
25
30
  export function CollectionTableToolbar({
@@ -29,6 +34,7 @@ export function CollectionTableToolbar({
29
34
  onTextSearch,
30
35
  onTextSearchClick,
31
36
  textSearchLoading,
37
+ initialSearchString,
32
38
  title,
33
39
  viewModeToggle
34
40
  }: CollectionTableToolbarProps) {
@@ -79,6 +85,7 @@ export function CollectionTableToolbar({
79
85
  disabled={Boolean(onTextSearchClick)}
80
86
  onClick={onTextSearchClick}
81
87
  onTextSearch={onTextSearchClick ? undefined : onTextSearch}
88
+ initialValue={initialSearchString}
82
89
  placeholder={t("search")}
83
90
  expandable={true} />}
84
91
 
@@ -106,6 +106,13 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
106
106
  * Full path using navigation ids.
107
107
  */
108
108
  fullIdPath?: string;
109
+ /**
110
+ * `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
111
+ * Forwarded to the datasource so a parent entity id containing "/" stays unambiguous.
112
+ * Defaults to splitting `fullPath`, which is correct for any backend whose ids cannot
113
+ * contain "/".
114
+ */
115
+ pathSegments?: string[];
109
116
  /**
110
117
  * If this is a subcollection, specify the parent collection ids.
111
118
  */
@@ -152,6 +159,7 @@ export const EntityCollectionView = React.memo(
152
159
  function EntityCollectionView<M extends Record<string, any>>({
153
160
  fullPath: fullPathProp,
154
161
  fullIdPath,
162
+ pathSegments,
155
163
  parentCollectionIds,
156
164
  isSubCollection,
157
165
  className,
@@ -178,7 +186,9 @@ export const EntityCollectionView = React.memo(
178
186
 
179
187
  const collection = useMemo(() => {
180
188
  const userOverride = userConfigPersistence?.getCollectionConfig<M>(fullPath);
181
- return (userOverride ? mergeDeep(collectionProp, userOverride) : collectionProp) as EntityCollection<M>;
189
+ if (!userOverride) return collectionProp;
190
+ const { properties, ...rest } = userOverride;
191
+ return mergeDeep(collectionProp, rest) as EntityCollection<M>;
182
192
  }, [collectionProp, fullPath, userConfigPersistence?.getCollectionConfig]);
183
193
 
184
194
  const openEntityMode = collection?.openEntityMode ?? DEFAULT_ENTITY_OPEN_MODE;
@@ -315,6 +325,7 @@ export const EntityCollectionView = React.memo(
315
325
 
316
326
  const tableController = useDataSourceTableController<M>({
317
327
  fullPath,
328
+ pathSegments,
318
329
  collection,
319
330
  lastDeleteTimestamp,
320
331
  scrollRestoration,
@@ -363,6 +374,7 @@ export const EntityCollectionView = React.memo(
363
374
  collection,
364
375
  entityId: undefined,
365
376
  path: fullPath,
377
+ pathSegments,
366
378
  fullIdPath,
367
379
  sideEntityController,
368
380
  navigation,
@@ -438,6 +450,7 @@ export const EntityCollectionView = React.memo(
438
450
  const onViewModeChange = useCallback((mode: ViewMode) => {
439
451
  analyticsController.onAnalyticsEvent?.("view_mode_changed", {
440
452
  path: fullPath,
453
+ pathSegments,
441
454
  from: viewMode,
442
455
  to: mode
443
456
  });
@@ -507,7 +520,8 @@ export const EntityCollectionView = React.memo(
507
520
  path: fullPath,
508
521
  propertyConfigs: customizationController.propertyConfigs,
509
522
  authController,
510
- }), [collection, fullPath]);
523
+ userConfigPersistence,
524
+ }), [collection, fullPath, userConfigPersistence]);
511
525
 
512
526
  // Check if Kanban view is possible (collection has at least one string enum property)
513
527
  const hasEnumProperty = useMemo(() => {
@@ -578,6 +592,7 @@ export const EntityCollectionView = React.memo(
578
592
  const onKanbanPropertyChange = useCallback((property: string) => {
579
593
  analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
580
594
  path: fullPath,
595
+ pathSegments,
581
596
  property
582
597
  });
583
598
  setSelectedKanbanProperty(property);
@@ -642,6 +657,7 @@ export const EntityCollectionView = React.memo(
642
657
  entityId: entity.id,
643
658
  selectedTab: subcollection.id ?? subcollection.path,
644
659
  path: fullPath,
660
+ pathSegments,
645
661
  fullIdPath,
646
662
  navigation,
647
663
  sideEntityController
@@ -828,7 +844,11 @@ export const EntityCollectionView = React.memo(
828
844
  } = useTableSearchHelper({
829
845
  collection,
830
846
  fullPath: resolvedFullPath,
831
- parentCollectionIds
847
+ parentCollectionIds,
848
+ // When the search term comes from the URL instead of the search bar, text search
849
+ // has never been initialised for this collection. Initialise it so the search bar
850
+ // is usable (editable and clearable) right away.
851
+ initialSearchString: tableController.searchString
832
852
  });
833
853
 
834
854
  // Popover open state managed at parent level to prevent closing when view changes
@@ -878,6 +898,7 @@ export const EntityCollectionView = React.memo(
878
898
  loading={tableController.dataLoading}
879
899
  onTextSearch={textSearchEnabled && textSearchInitialised ? tableController.setSearchString : undefined}
880
900
  onTextSearchClick={textSearchEnabled && !textSearchInitialised ? onTextSearchClick : undefined}
901
+ initialSearchString={tableController.searchString}
881
902
  textSearchLoading={textSearchLoading}
882
903
  viewModeToggle={viewModeToggleElement}
883
904
  actionsStart={<EntityCollectionViewStartActions
@@ -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);
@@ -11,6 +11,7 @@ import {
11
11
  TextField
12
12
  } from "@firecms/ui";
13
13
  import { EnumValueConfig } from "../../../types";
14
+ import { filterValueToText, parseFilterTextInput } from "./filter_text_input";
14
15
 
15
16
  interface StringNumberFilterFieldProps {
16
17
  name: string,
@@ -60,8 +61,15 @@ export function StringNumberFilterField({
60
61
  const [fieldOperation, fieldValue] = value || [possibleOperations[0], undefined];
61
62
  const [operation, setOperation] = useState<VirtualTableWhereFilterOp | "is-null">(fieldOperation === "==" && fieldValue === null ? "is-null" : fieldOperation);
62
63
  const [internalValue, setInternalValue] = useState<string | number | string[] | number[] | null | undefined>(fieldValue);
64
+ // Raw text of the text input, kept apart from `internalValue` so that intermediate
65
+ // inputs like "-", "0." or ".0" are not destroyed while the user is typing.
66
+ const [inputText, setInputText] = useState<string>(filterValueToText(fieldValue));
67
+ // Value the current `inputText` was built from, used to tell updates coming from
68
+ // outside apart from the ones triggered by the user typing.
69
+ const syncedValueRef = React.useRef(fieldValue);
63
70
 
64
71
  React.useEffect(() => {
72
+ const newValue = value ? value[1] : undefined;
65
73
  if (value) {
66
74
  const [op, val] = value;
67
75
  setOperation(op === "==" && val === null ? "is-null" : op);
@@ -70,15 +78,25 @@ export function StringNumberFilterField({
70
78
  setOperation(possibleOperations[0]);
71
79
  setInternalValue(undefined);
72
80
  }
81
+ // only reset the text when the value was changed somewhere else, so we don't
82
+ // interfere with what the user is currently typing
83
+ if (syncedValueRef.current !== newValue) {
84
+ syncedValueRef.current = newValue;
85
+ setInputText(filterValueToText(newValue));
86
+ }
73
87
  }, [value, possibleOperations[0]]);
74
88
 
75
89
  const isNullOperation = operation === "is-null";
76
90
 
77
- function updateFilter(op: VirtualTableWhereFilterOp | "is-null", val: string | number | string[] | number[] | null | undefined) {
91
+ // `keepInputText` is used when the update originates from the text input, where the
92
+ // text typed by the user is authoritative and must not be overwritten with `val`
93
+ function updateFilter(op: VirtualTableWhereFilterOp | "is-null", val: string | number | string[] | number[] | null | undefined, keepInputText = false) {
78
94
  // Handle "is null" operation
79
95
  if (op === "is-null") {
80
96
  setOperation(op);
81
97
  setInternalValue(null);
98
+ syncedValueRef.current = null;
99
+ setInputText("");
82
100
  setValue(["==", null]);
83
101
  return;
84
102
  }
@@ -96,6 +114,9 @@ export function StringNumberFilterField({
96
114
 
97
115
  setOperation(op);
98
116
  setInternalValue(newValue);
117
+ syncedValueRef.current = newValue;
118
+ if (!keepInputText)
119
+ setInputText(filterValueToText(newValue));
99
120
 
100
121
  const hasNewValue = newValue !== null && Array.isArray(newValue)
101
122
  ? newValue.length > 0
@@ -111,6 +132,14 @@ export function StringNumberFilterField({
111
132
  }
112
133
  }
113
134
 
135
+ function updateFilterFromText(text: string) {
136
+ setInputText(text);
137
+ const { updateValue, value: parsedValue } = parseFilterTextInput(text, dataType);
138
+ // intermediate inputs like "-" or "0." keep the filter as it is
139
+ if (updateValue)
140
+ updateFilter(operation, parsedValue, true);
141
+ }
142
+
114
143
  const multiple = multipleSelectOperations.includes(operation);
115
144
 
116
145
  return (
@@ -138,16 +167,13 @@ export function StringNumberFilterField({
138
167
  {!enumValues && <TextField
139
168
  size={"medium"}
140
169
  type={dataType === "number" ? "number" : undefined}
141
- value={internalValue !== undefined && internalValue != null ? String(internalValue) : ""}
170
+ value={inputText}
142
171
  disabled={isNullOperation}
143
172
  placeholder={isNullOperation ? "null" : undefined}
144
173
  onChange={(evt) => {
145
- const val = dataType === "number"
146
- ? parseFloat(evt.target.value)
147
- : evt.target.value;
148
- updateFilter(operation, val);
174
+ updateFilterFromText(evt.target.value);
149
175
  }}
150
- endAdornment={internalValue !== undefined && internalValue != null && <IconButton
176
+ endAdornment={inputText !== "" && <IconButton
151
177
  onClick={(e) => updateFilter(operation, undefined)}>
152
178
  <CloseIcon />
153
179
  </IconButton>}