@firecms/core 3.3.0 → 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 (108) 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 +2790 -1270
  20. package/dist/index.es.js.map +1 -1
  21. package/dist/index.umd.js +2788 -1268
  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 +6 -0
  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 +34 -44
  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 +19 -1
  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/ReferenceWidget.tsx +1 -1
  53. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  54. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  55. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  56. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  57. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  58. package/src/components/common/table_url_params.ts +172 -0
  59. package/src/components/common/useDataSourceTableController.tsx +98 -159
  60. package/src/components/common/useTableSearchHelper.ts +21 -2
  61. package/src/contexts/SnackbarProvider.tsx +14 -1
  62. package/src/core/DefaultDrawer.tsx +150 -64
  63. package/src/core/DrawerNavigationGroup.tsx +27 -29
  64. package/src/core/DrawerNavigationItem.tsx +10 -12
  65. package/src/core/EntityEditView.tsx +75 -32
  66. package/src/core/EntitySidePanel.tsx +2 -0
  67. package/src/core/field_configs.tsx +15 -0
  68. package/src/form/EntityForm.tsx +1 -1
  69. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  70. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  71. package/src/form/index.tsx +1 -0
  72. package/src/hooks/data/save.ts +2 -0
  73. package/src/hooks/data/useEntityFetch.tsx +9 -0
  74. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  75. package/src/i18n/FireCMSi18nProvider.tsx +2 -0
  76. package/src/internal/useBuildDataSource.ts +11 -0
  77. package/src/internal/useBuildSideEntityController.tsx +8 -1
  78. package/src/locales/de.ts +7 -2
  79. package/src/locales/en.ts +7 -2
  80. package/src/locales/es.ts +7 -2
  81. package/src/locales/fr.ts +7 -2
  82. package/src/locales/hi.ts +7 -2
  83. package/src/locales/it.ts +7 -2
  84. package/src/locales/pl.ts +735 -0
  85. package/src/locales/pt.ts +7 -2
  86. package/src/preview/PropertyPreview.tsx +12 -0
  87. package/src/preview/components/StorageThumbnail.tsx +24 -2
  88. package/src/preview/index.ts +1 -0
  89. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  90. package/src/routes/FireCMSRoute.tsx +47 -25
  91. package/src/types/collections.ts +26 -0
  92. package/src/types/datasource.ts +42 -0
  93. package/src/types/properties.ts +17 -0
  94. package/src/types/side_entity_controller.tsx +6 -0
  95. package/src/types/translations.ts +6 -0
  96. package/src/util/__tests__/collections.test.ts +32 -0
  97. package/src/util/__tests__/urls.test.ts +131 -0
  98. package/src/util/collections.ts +1 -1
  99. package/src/util/entities.ts +13 -0
  100. package/src/util/geopoint.ts +81 -0
  101. package/src/util/index.ts +2 -0
  102. package/src/util/navigation_blocking.ts +45 -0
  103. package/src/util/navigation_from_path.ts +41 -6
  104. package/src/util/navigation_utils.ts +40 -2
  105. package/src/util/parent_references_from_path.ts +4 -2
  106. package/src/util/previews.ts +8 -1
  107. package/src/util/resolutions.ts +8 -0
  108. package/src/util/urls.ts +52 -0
@@ -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,
@@ -317,6 +325,7 @@ export const EntityCollectionView = React.memo(
317
325
 
318
326
  const tableController = useDataSourceTableController<M>({
319
327
  fullPath,
328
+ pathSegments,
320
329
  collection,
321
330
  lastDeleteTimestamp,
322
331
  scrollRestoration,
@@ -365,6 +374,7 @@ export const EntityCollectionView = React.memo(
365
374
  collection,
366
375
  entityId: undefined,
367
376
  path: fullPath,
377
+ pathSegments,
368
378
  fullIdPath,
369
379
  sideEntityController,
370
380
  navigation,
@@ -440,6 +450,7 @@ export const EntityCollectionView = React.memo(
440
450
  const onViewModeChange = useCallback((mode: ViewMode) => {
441
451
  analyticsController.onAnalyticsEvent?.("view_mode_changed", {
442
452
  path: fullPath,
453
+ pathSegments,
443
454
  from: viewMode,
444
455
  to: mode
445
456
  });
@@ -581,6 +592,7 @@ export const EntityCollectionView = React.memo(
581
592
  const onKanbanPropertyChange = useCallback((property: string) => {
582
593
  analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
583
594
  path: fullPath,
595
+ pathSegments,
584
596
  property
585
597
  });
586
598
  setSelectedKanbanProperty(property);
@@ -645,6 +657,7 @@ export const EntityCollectionView = React.memo(
645
657
  entityId: entity.id,
646
658
  selectedTab: subcollection.id ?? subcollection.path,
647
659
  path: fullPath,
660
+ pathSegments,
648
661
  fullIdPath,
649
662
  navigation,
650
663
  sideEntityController
@@ -831,7 +844,11 @@ export const EntityCollectionView = React.memo(
831
844
  } = useTableSearchHelper({
832
845
  collection,
833
846
  fullPath: resolvedFullPath,
834
- 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
835
852
  });
836
853
 
837
854
  // Popover open state managed at parent level to prevent closing when view changes
@@ -881,6 +898,7 @@ export const EntityCollectionView = React.memo(
881
898
  loading={tableController.dataLoading}
882
899
  onTextSearch={textSearchEnabled && textSearchInitialised ? tableController.setSearchString : undefined}
883
900
  onTextSearchClick={textSearchEnabled && !textSearchInitialised ? onTextSearchClick : undefined}
901
+ initialSearchString={tableController.searchString}
884
902
  textSearchLoading={textSearchLoading}
885
903
  viewModeToggle={viewModeToggleElement}
886
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
+
@@ -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>}