@firecms/core 3.0.0-beta.5 → 3.0.0-beta.6

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 (67) hide show
  1. package/dist/components/ClearFilterSortButton.d.ts +5 -0
  2. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +11 -11
  3. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +1 -1
  4. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +1 -4
  5. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +11 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +11 -0
  7. package/dist/components/SelectableTable/SelectableTable.d.ts +1 -1
  8. package/dist/components/common/types.d.ts +1 -1
  9. package/dist/form/components/ErrorFocus.d.ts +1 -1
  10. package/dist/hooks/useValidateAuthenticator.d.ts +2 -6
  11. package/dist/index.es.js +3516 -3381
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/index.umd.js +5 -5
  14. package/dist/index.umd.js.map +1 -1
  15. package/dist/internal/useBuildDataSource.d.ts +1 -16
  16. package/dist/types/collections.d.ts +5 -1
  17. package/dist/types/datasource.d.ts +2 -5
  18. package/dist/types/entities.d.ts +5 -1
  19. package/dist/types/index.d.ts +0 -1
  20. package/dist/types/navigation.d.ts +1 -1
  21. package/dist/types/plugins.d.ts +3 -1
  22. package/dist/types/properties.d.ts +2 -2
  23. package/dist/util/entities.d.ts +1 -1
  24. package/dist/util/navigation_utils.d.ts +2 -2
  25. package/dist/util/resolutions.d.ts +5 -5
  26. package/package.json +22 -19
  27. package/src/components/ClearFilterSortButton.tsx +41 -0
  28. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +10 -11
  29. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +1 -1
  30. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +16 -19
  31. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +27 -32
  32. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +11 -6
  33. package/src/components/EntityCollectionView/EntityCollectionView.tsx +28 -5
  34. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +68 -0
  35. package/src/components/EntityView.tsx +1 -1
  36. package/src/components/HomePage/DefaultHomePage.tsx +1 -1
  37. package/src/components/SelectableTable/SelectableTable.tsx +1 -1
  38. package/src/components/SelectableTable/filters/BooleanFilterField.tsx +2 -3
  39. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +22 -7
  40. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +24 -5
  41. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +35 -15
  42. package/src/components/VirtualTable/VirtualTable.tsx +28 -20
  43. package/src/components/common/types.tsx +1 -1
  44. package/src/core/FireCMS.tsx +1 -2
  45. package/src/core/field_configs.tsx +1 -2
  46. package/src/hooks/data/save.ts +1 -0
  47. package/src/hooks/useBuildLocalConfigurationPersistence.tsx +9 -10
  48. package/src/hooks/useBuildModeController.tsx +11 -5
  49. package/src/hooks/useBuildNavigationController.tsx +76 -19
  50. package/src/hooks/useProjectLog.tsx +3 -3
  51. package/src/hooks/useValidateAuthenticator.tsx +2 -22
  52. package/src/internal/useBuildDataSource.ts +42 -47
  53. package/src/internal/useBuildSideEntityController.tsx +16 -13
  54. package/src/preview/PropertyPreview.tsx +2 -12
  55. package/src/types/collections.ts +5 -1
  56. package/src/types/datasource.ts +7 -4
  57. package/src/types/entities.ts +9 -1
  58. package/src/types/index.ts +0 -1
  59. package/src/types/navigation.ts +1 -1
  60. package/src/types/plugins.tsx +4 -3
  61. package/src/types/properties.ts +5 -4
  62. package/src/util/entities.ts +1 -1
  63. package/src/util/navigation_utils.ts +6 -6
  64. package/dist/internal/useLocaleConfig.d.ts +0 -1
  65. package/dist/types/appcheck.d.ts +0 -26
  66. package/src/internal/useLocaleConfig.tsx +0 -18
  67. package/src/types/appcheck.ts +0 -29
@@ -96,7 +96,7 @@ export const EntityTableCell = React.memo<EntityTableCellProps>(
96
96
  const [onHover, setOnHover] = useState(false);
97
97
  const [internalSaved, setInternalSaved] = useState(saved);
98
98
 
99
- const showError = !disabled && error;
99
+ const showError = !disabled && Boolean(error);
100
100
 
101
101
  useEffect(() => {
102
102
  if (saved) {
@@ -176,21 +176,26 @@ export const EntityTableCell = React.memo<EntityTableCellProps>(
176
176
  const setOnHoverTrue = useCallback(() => setOnHover(true), []);
177
177
  const setOnHoverFalse = useCallback(() => setOnHover(false), []);
178
178
 
179
+ const borderClass = showError
180
+ ? "border-red-500"
181
+ : internalSaved
182
+ ? "border-green-500"
183
+ : isSelected
184
+ ? "border-primary"
185
+ : "border-transparent";
186
+
179
187
  return (
180
188
  <div
181
- ref={ref}
182
189
  className={cn(
183
190
  "transition-colors duration-100 ease-in-out",
184
191
  `flex relative h-full rounded-md p-${p} border border-4 border-opacity-75`,
185
192
  onHover && !disabled ? "bg-gray-50 dark:bg-gray-900" : "",
186
193
  saved ? "bg-gray-100 bg-opacity-75 dark:bg-gray-800 dark:bg-opacity-75" : "",
187
- !isSelected && !internalSaved && !showError ? "border-transparent" : "",
188
194
  hideOverflow ? "overflow-hidden" : "",
189
195
  isSelected ? "bg-gray-50 dark:bg-gray-900" : "",
190
- isSelected && !internalSaved ? "border-primary" : "",
191
- internalSaved ? "border-green-500 " : "",
192
- showError ? "border-red-500" : ""
196
+ borderClass
193
197
  )}
198
+ ref={ref}
194
199
  style={{
195
200
  justifyContent,
196
201
  alignItems: disabled || !isOverflowing ? "center" : undefined,
@@ -73,6 +73,7 @@ import {
73
73
  import { DeleteEntityDialog } from "../DeleteEntityDialog";
74
74
  import { useAnalyticsController } from "../../hooks/useAnalyticsController";
75
75
  import { useSelectionController } from "./useSelectionController";
76
+ import { EntityCollectionViewStartActions } from "./EntityCollectionViewStartActions";
76
77
 
77
78
  const COLLECTION_GROUP_PARENT_ID = "collectionGroupParent";
78
79
 
@@ -80,8 +81,18 @@ const COLLECTION_GROUP_PARENT_ID = "collectionGroupParent";
80
81
  * @group Components
81
82
  */
82
83
  export type EntityCollectionViewProps<M extends Record<string, any>> = {
83
- fullPath: string;
84
+ /**
85
+ * Complete path where this collection is located.
86
+ * It defaults to the collection path if not provided.
87
+ */
88
+ fullPath?: string;
89
+ /**
90
+ * If this is a subcollection, specify the parent collection ids.
91
+ */
84
92
  parentCollectionIds?: string[];
93
+ /**
94
+ * Whether this is a subcollection or not.
95
+ */
85
96
  isSubCollection?: boolean;
86
97
  className?: string;
87
98
  } & EntityCollection<M>;
@@ -112,7 +123,7 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
112
123
  */
113
124
  export const EntityCollectionView = React.memo(
114
125
  function EntityCollectionView<M extends Record<string, any>>({
115
- fullPath,
126
+ fullPath: fullPathProp,
116
127
  parentCollectionIds,
117
128
  isSubCollection,
118
129
  className,
@@ -120,6 +131,7 @@ export const EntityCollectionView = React.memo(
120
131
  }: EntityCollectionViewProps<M>
121
132
  ) {
122
133
 
134
+ const fullPath = fullPathProp ?? collectionProp.path;
123
135
  const dataSource = useDataSource(collectionProp);
124
136
  const navigation = useNavigationController();
125
137
  const sideEntityController = useSideEntityController();
@@ -128,7 +140,6 @@ export const EntityCollectionView = React.memo(
128
140
  const analyticsController = useAnalyticsController();
129
141
  const customizationController = useCustomizationController();
130
142
 
131
-
132
143
  const containerRef = React.useRef<HTMLDivElement>(null);
133
144
 
134
145
  const collection = useMemo(() => {
@@ -175,7 +186,6 @@ export const EntityCollectionView = React.memo(
175
186
  const usedSelectionController = collection.selectionController ?? selectionController;
176
187
  const {
177
188
  selectedEntities,
178
- toggleEntitySelection,
179
189
  isEntitySelected,
180
190
  setSelectedEntities
181
191
  } = usedSelectionController;
@@ -327,7 +337,10 @@ export const EntityCollectionView = React.memo(
327
337
  callbacks: collection.callbacks,
328
338
  dataSource,
329
339
  context,
330
- onSaveSuccess: () => onValueUpdated(),
340
+ onSaveSuccess: () => {
341
+ setError(undefined);
342
+ onValueUpdated();
343
+ },
331
344
  onSaveFailure: (e: Error) => {
332
345
  console.error("Save failure");
333
346
  console.error(e);
@@ -606,6 +619,14 @@ export const EntityCollectionView = React.memo(
606
619
  onTextSearchClick={textSearchInitialised ? undefined : onTextSearchClick}
607
620
  textSearchLoading={textSearchLoading}
608
621
  textSearchEnabled={textSearchEnabled}
622
+ actionsStart={<EntityCollectionViewStartActions
623
+ parentCollectionIds={parentCollectionIds ?? []}
624
+ collection={collection}
625
+ tableController={tableController}
626
+ path={fullPath}
627
+ relativePath={collection.path}
628
+ selectionController={usedSelectionController}
629
+ collectionEntitiesCount={docsCount}/>}
609
630
  actions={<EntityCollectionViewActions
610
631
  parentCollectionIds={parentCollectionIds ?? []}
611
632
  collection={collection}
@@ -683,6 +704,8 @@ export const EntityCollectionView = React.memo(
683
704
  equal(a.selectionController, b.selectionController) &&
684
705
  equal(a.Actions, b.Actions) &&
685
706
  equal(a.defaultSize, b.defaultSize) &&
707
+ equal(a.initialFilter, b.initialFilter) &&
708
+ equal(a.initialSort, b.initialSort) &&
686
709
  equal(a.textSearchEnabled, b.textSearchEnabled) &&
687
710
  equal(a.additionalFields, b.additionalFields) &&
688
711
  equal(a.forceFilter, b.forceFilter);
@@ -0,0 +1,68 @@
1
+ import React from "react";
2
+ import { useCustomizationController, useFireCMSContext } from "../../hooks";
3
+ import { CollectionActionsProps, EntityCollection, EntityTableController, SelectionController } from "../../types";
4
+ import { toArray } from "../../util/arrays";
5
+ import { ErrorBoundary } from "../ErrorBoundary";
6
+ import { ClearFilterSortButton } from "../ClearFilterSortButton";
7
+
8
+ export type EntityCollectionViewStartActionsProps<M extends Record<string, any>> = {
9
+ collection: EntityCollection<M>;
10
+ path: string;
11
+ relativePath: string;
12
+ parentCollectionIds: string[];
13
+ selectionController: SelectionController<M>;
14
+ tableController: EntityTableController<M>;
15
+ collectionEntitiesCount: number;
16
+ }
17
+
18
+ export function EntityCollectionViewStartActions<M extends Record<string, any>>({
19
+ collection,
20
+ relativePath,
21
+ parentCollectionIds,
22
+ path,
23
+ selectionController,
24
+ tableController,
25
+ collectionEntitiesCount
26
+ }: EntityCollectionViewStartActionsProps<M>) {
27
+
28
+ const context = useFireCMSContext();
29
+
30
+ const customizationController = useCustomizationController();
31
+ const plugins = customizationController.plugins ?? [];
32
+
33
+ const actionProps: CollectionActionsProps = {
34
+ path,
35
+ relativePath,
36
+ parentCollectionIds,
37
+ collection,
38
+ selectionController,
39
+ context,
40
+ tableController,
41
+ collectionEntitiesCount
42
+ };
43
+ const actions: React.ReactNode[] = [
44
+ <ClearFilterSortButton
45
+ key={"clear_filter"}
46
+ tableController={tableController}
47
+ enabled={!collection.forceFilter}/>
48
+ ];
49
+
50
+ if (plugins) {
51
+ plugins.forEach((plugin, i) => {
52
+ if (plugin.collectionView?.CollectionActionsStart) {
53
+ actions.push(...toArray(plugin.collectionView?.CollectionActionsStart)
54
+ .map((Action, j) => (
55
+ <ErrorBoundary key={`plugin_actions_${i}_${j}`}>
56
+ <Action {...actionProps} {...plugin.collectionView?.collectionActionsStartProps}/>
57
+ </ErrorBoundary>
58
+ )));
59
+ }
60
+ });
61
+ }
62
+
63
+ return (
64
+ <>
65
+ {actions}
66
+ </>
67
+ );
68
+ }
@@ -58,7 +58,7 @@ export function EntityView<M extends Record<string, any>>(
58
58
  </div>
59
59
  {Object.entries(properties)
60
60
  .map(([key, property]) => {
61
- const value = (entity.values)[key];
61
+ const value = entity.values?.[key];
62
62
  return (
63
63
  <div
64
64
  key={`reference_previews_${key}`}
@@ -191,7 +191,7 @@ export function DefaultHomePage({
191
191
  />
192
192
  </div>
193
193
  ))}
194
- {AdditionalCards &&
194
+ {group?.toLowerCase() !== "admin" && AdditionalCards &&
195
195
  AdditionalCards.map((AdditionalCard, i) => (
196
196
  <div key={`nav_${group}_add_${i}`}>
197
197
  <AdditionalCard {...actionProps} />
@@ -10,7 +10,7 @@ import {
10
10
  } from "../../types";
11
11
  import { CellRendererParams, VirtualTable, VirtualTableColumn } from "../VirtualTable";
12
12
  import { enumToObjectEntries } from "../../util";
13
- import { OnCellValueChange, OnColumnResizeParams } from "../common/types";
13
+ import { OnCellValueChange, OnColumnResizeParams } from "../common";
14
14
  import { FilterFormFieldProps } from "../VirtualTable/VirtualTableHeader";
15
15
  import { ReferenceFilterField } from "./filters/ReferenceFilterField";
16
16
  import { StringNumberFilterField } from "./filters/StringNumberFilterField";
@@ -32,7 +32,7 @@ export function BooleanFilterField({
32
32
  const valueSet = !!value;
33
33
 
34
34
  return (
35
- <div className="w-[200px]">
35
+ <div className="w-[300px]">
36
36
  <BooleanSwitchWithLabel
37
37
  value={valueSetToTrue}
38
38
  allowIndeterminate={true}
@@ -44,6 +44,5 @@ export function BooleanFilterField({
44
44
  : `${title} is false`}
45
45
  />
46
46
  </div>
47
- )
48
- ;
47
+ );
49
48
  }
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from "react";
2
2
  import { VirtualTableWhereFilterOp } from "../../VirtualTable";
3
- import { DateTimeField, Select, SelectItem } from "@firecms/ui";
3
+ import { Checkbox, DateTimeField, Label, Select, SelectItem } from "@firecms/ui";
4
4
  import { useCustomizationController } from "../../../hooks";
5
5
 
6
6
  interface DateTimeFilterFieldProps {
@@ -43,10 +43,10 @@ export function DateTimeFilterField({
43
43
 
44
44
  const [fieldOperation, fieldValue] = value || [possibleOperations[0], undefined];
45
45
  const [operation, setOperation] = useState<VirtualTableWhereFilterOp>(fieldOperation);
46
- const [internalValue, setInternalValue] = useState<Date | undefined>(fieldValue);
46
+ const [internalValue, setInternalValue] = useState<Date | null | undefined>(fieldValue);
47
47
 
48
- function updateFilter(op: VirtualTableWhereFilterOp, val: Date | undefined) {
49
- let newValue: Date | undefined = val;
48
+ function updateFilter(op: VirtualTableWhereFilterOp, val: Date | undefined | null) {
49
+ let newValue: Date | null | undefined = val;
50
50
  const prevOpIsArray = multipleSelectOperations.includes(operation);
51
51
  const newOpIsArray = multipleSelectOperations.includes(op);
52
52
  if (prevOpIsArray !== newOpIsArray) {
@@ -73,7 +73,7 @@ export function DateTimeFilterField({
73
73
 
74
74
  return (
75
75
 
76
- <div className="flex w-[440px] items-center">
76
+ <div className="flex w-[440px]">
77
77
  <div className="w-[80px]">
78
78
  <Select value={operation}
79
79
  onValueChange={(value) => {
@@ -88,19 +88,34 @@ export function DateTimeFilterField({
88
88
  </Select>
89
89
  </div>
90
90
 
91
- <div className="flex-grow ml-2">
91
+ <div className="flex-grow ml-2 flex flex-col gap-2">
92
92
 
93
93
  <DateTimeField
94
94
  mode={mode}
95
95
  size={"medium"}
96
96
  locale={locale}
97
- value={internalValue}
97
+ value={internalValue ?? undefined}
98
98
  onChange={(dateValue: Date | undefined) => {
99
99
  updateFilter(operation, dateValue === null ? undefined : dateValue);
100
100
  }}
101
101
  clearable={true}
102
102
  />
103
103
 
104
+ <Label
105
+ className="border cursor-pointer rounded-md p-2 flex items-center gap-2 [&:has(:checked)]:bg-gray-100 dark:[&:has(:checked)]:bg-gray-800"
106
+ htmlFor="null-filter"
107
+ >
108
+ <Checkbox id="null-filter"
109
+ checked={internalValue === null}
110
+ size={"small"}
111
+ onCheckedChange={(checked) => {
112
+ if (internalValue !== null)
113
+ updateFilter(operation, null);
114
+ else updateFilter(operation, undefined);
115
+ }}/>
116
+ Filter for null values
117
+ </Label>
118
+
104
119
  </div>
105
120
 
106
121
  </div>
@@ -4,7 +4,7 @@ import { Entity, EntityCollection, EntityReference } from "../../../types";
4
4
  import { ReferencePreview } from "../../../preview";
5
5
  import { getReferenceFrom } from "../../../util";
6
6
  import { useNavigationController, useReferenceDialog } from "../../../hooks";
7
- import { Button, Select, SelectItem } from "@firecms/ui";
7
+ import { Button, Checkbox, Label, Select, SelectItem } from "@firecms/ui";
8
8
 
9
9
  interface ReferenceFilterFieldProps {
10
10
  name: string,
@@ -54,7 +54,7 @@ export function ReferenceFilterField({
54
54
 
55
55
  const [fieldOperation, fieldValue] = value || [possibleOperations[0], undefined];
56
56
  const [operation, setOperation] = useState<VirtualTableWhereFilterOp>(fieldOperation);
57
- const [internalValue, setInternalValue] = useState<EntityReference | EntityReference[] | undefined>(fieldValue);
57
+ const [internalValue, setInternalValue] = useState<EntityReference | EntityReference[] | undefined | null>(fieldValue);
58
58
 
59
59
  const selectedEntityIds = internalValue
60
60
  ? (Array.isArray(internalValue) ? internalValue.map((ref) => {
@@ -65,7 +65,7 @@ export function ReferenceFilterField({
65
65
  }).filter(Boolean) as string[] : [internalValue.id])
66
66
  : [];
67
67
 
68
- function updateFilter(op: VirtualTableWhereFilterOp, val?: EntityReference | EntityReference[]) {
68
+ function updateFilter(op: VirtualTableWhereFilterOp, val?: EntityReference | EntityReference[] | null) {
69
69
 
70
70
  const prevOpIsArray = multipleSelectOperations.includes(operation);
71
71
  const newOpIsArray = multipleSelectOperations.includes(op);
@@ -142,7 +142,7 @@ export function ReferenceFilterField({
142
142
  return (
143
143
 
144
144
  <div className="flex w-[440px] flex-row">
145
- <div className="w-[120px]">
145
+ <div className="w-[140px]">
146
146
  <Select value={operation}
147
147
  onValueChange={(value) => {
148
148
  updateFilter(value as VirtualTableWhereFilterOp, internalValue);
@@ -156,21 +156,40 @@ export function ReferenceFilterField({
156
156
  </Select>
157
157
  </div>
158
158
 
159
- <div className="flex-grow ml-2 h-full">
159
+ <div className="flex-grow ml-2 h-full gap-2 flex flex-col">
160
160
 
161
161
  {internalValue && Array.isArray(internalValue) && <div>
162
162
  {internalValue.map((ref, index) => buildEntry(ref))}
163
163
  </div>}
164
+
164
165
  {internalValue && !Array.isArray(internalValue) && <div>
165
166
  {buildEntry(internalValue)}
166
167
  </div>}
168
+
167
169
  {(!internalValue || (Array.isArray(internalValue) && internalValue.length === 0)) &&
168
170
  <Button onClick={doOpenDialog}
169
171
  variant={"outlined"}
172
+ size={"large"}
170
173
  className="h-full w-full">
171
174
  {multiple ? "Select references" : "Select reference"}
172
175
  </Button>
173
176
  }
177
+
178
+ {!isArray && <Label
179
+ className="border cursor-pointer rounded-md p-2 flex items-center gap-2 [&:has(:checked)]:bg-gray-100 dark:[&:has(:checked)]:bg-gray-800"
180
+ htmlFor="null-filter"
181
+ >
182
+ <Checkbox id="null-filter"
183
+ checked={internalValue === null}
184
+ size={"small"}
185
+ onCheckedChange={(checked) => {
186
+ if (internalValue !== null)
187
+ updateFilter(operation, null);
188
+ else updateFilter(operation, undefined);
189
+ }}/>
190
+ Filter for null values
191
+ </Label>}
192
+
174
193
  </div>
175
194
 
176
195
  </div>
@@ -1,7 +1,7 @@
1
1
  import React, { useState } from "react";
2
2
  import { EnumValuesChip } from "../../../preview";
3
3
  import { VirtualTableWhereFilterOp } from "../../VirtualTable";
4
- import { ClearIcon, IconButton, Select, SelectItem, TextField } from "@firecms/ui";
4
+ import { Checkbox, ClearIcon, IconButton, Label, Select, SelectItem, TextField } from "@firecms/ui";
5
5
  import { EnumValueConfig } from "../../../types";
6
6
 
7
7
  interface StringNumberFilterFieldProps {
@@ -50,15 +50,15 @@ export function StringNumberFilterField({
50
50
 
51
51
  const [fieldOperation, fieldValue] = value || [possibleOperations[0], undefined];
52
52
  const [operation, setOperation] = useState<VirtualTableWhereFilterOp>(fieldOperation);
53
- const [internalValue, setInternalValue] = useState<string | number | string[] | number[] | undefined>(fieldValue);
53
+ const [internalValue, setInternalValue] = useState<string | number | string[] | number[] | null | undefined>(fieldValue);
54
54
 
55
- function updateFilter(op: VirtualTableWhereFilterOp, val: string | number | string[] | number[] | undefined) {
55
+ function updateFilter(op: VirtualTableWhereFilterOp, val: string | number | string[] | number[] | null | undefined) {
56
56
  let newValue = val;
57
57
  const prevOpIsArray = multipleSelectOperations.includes(operation);
58
58
  const newOpIsArray = multipleSelectOperations.includes(op);
59
59
  if (prevOpIsArray !== newOpIsArray) {
60
60
  // @ts-ignore
61
- newValue = newOpIsArray ? (typeof val === "string" || typeof val === "number" ? [val] : []) : "";
61
+ newValue = newOpIsArray ? (typeof val === "string" || typeof val === "number" ? [val] : []) : undefined;
62
62
  }
63
63
 
64
64
  if (typeof newValue === "number" && isNaN(newValue))
@@ -84,7 +84,7 @@ export function StringNumberFilterField({
84
84
  const multiple = multipleSelectOperations.includes(operation);
85
85
  return (
86
86
 
87
- <div className="flex w-[440px] items-center">
87
+ <div className="flex w-[440px]">
88
88
  <div className={"w-[80px]"}>
89
89
  <Select value={operation}
90
90
  position={"item-aligned"}
@@ -100,11 +100,11 @@ export function StringNumberFilterField({
100
100
  </Select>
101
101
  </div>
102
102
 
103
- <div className="flex-grow ml-2">
103
+ <div className="flex-grow ml-2 flex flex-col gap-2">
104
104
 
105
105
  {!enumValues && <TextField
106
106
  type={dataType === "number" ? "number" : undefined}
107
- value={internalValue !== undefined ? String(internalValue) : ""}
107
+ value={internalValue !== undefined && internalValue != null ? String(internalValue) : ""}
108
108
  onChange={(evt) => {
109
109
  const val = dataType === "number"
110
110
  ? parseFloat(evt.target.value)
@@ -118,26 +118,31 @@ export function StringNumberFilterField({
118
118
  />}
119
119
 
120
120
  {enumValues &&
121
-
122
121
  <Select
123
122
  position={"item-aligned"}
124
123
  value={internalValue !== undefined
125
124
  ? (Array.isArray(internalValue) ? internalValue.map(e => String(e)) : String(internalValue))
126
125
  : isArray ? [] : ""}
127
126
  onValueChange={(value) => {
128
- updateFilter(operation, dataType === "number" ? parseInt(value as string) : value as string)
127
+ if (value !== "")
128
+ updateFilter(operation, dataType === "number" ? parseInt(value as string) : value as string)
129
129
  }}
130
130
  multiple={multiple}
131
131
  endAdornment={internalValue && <IconButton
132
- className="absolute right-3 top-2"
132
+ className="absolute right-2 top-3"
133
133
  onClick={(e) => updateFilter(operation, undefined)}>
134
134
  <ClearIcon/>
135
135
  </IconButton>}
136
- renderValue={(enumKey) => <EnumValuesChip
137
- key={`select_value_${name}_${enumKey}`}
138
- enumKey={enumKey}
139
- enumValues={enumValues}
140
- size={"small"}/>}>
136
+ renderValue={(enumKey) => {
137
+ if (enumKey === null)
138
+ return "Filter for null values";
139
+
140
+ return <EnumValuesChip
141
+ key={`select_value_${name}_${enumKey}`}
142
+ enumKey={enumKey}
143
+ enumValues={enumValues}
144
+ size={"small"}/>;
145
+ }}>
141
146
  {enumValues.map((enumConfig) => (
142
147
  <SelectItem key={`select_value_${name}_${enumConfig.id}`}
143
148
  value={String(enumConfig.id)}>
@@ -150,6 +155,21 @@ export function StringNumberFilterField({
150
155
  </Select>
151
156
  }
152
157
 
158
+ {!isArray && <Label
159
+ className="border cursor-pointer rounded-md p-2 flex items-center gap-2 [&:has(:checked)]:bg-gray-100 dark:[&:has(:checked)]:bg-gray-800"
160
+ htmlFor="null-filter"
161
+ >
162
+ <Checkbox id="null-filter"
163
+ checked={internalValue === null}
164
+ size={"small"}
165
+ onCheckedChange={(checked) => {
166
+ if (internalValue !== null)
167
+ updateFilter(operation, null);
168
+ else updateFilter(operation, undefined);
169
+ }}/>
170
+ Filter for null values
171
+ </Label>}
172
+
153
173
  </div>
154
174
 
155
175
  </div>
@@ -20,7 +20,7 @@ import { VirtualTableContextProps } from "./types";
20
20
  import { VirtualTableHeaderRow } from "./VirtualTableHeaderRow";
21
21
  import { VirtualTableRow } from "./VirtualTableRow";
22
22
  import { VirtualTableCell } from "./VirtualTableCell";
23
- import { AssignmentIcon, cn, Markdown, Typography } from "@firecms/ui";
23
+ import { AssignmentIcon, CenteredView, cn, Typography } from "@firecms/ui";
24
24
 
25
25
  const VirtualListContext = createContext<VirtualTableContextProps<any>>({} as any);
26
26
  VirtualListContext.displayName = "VirtualListContext";
@@ -225,24 +225,7 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
225
225
  }
226
226
 
227
227
  if (onFilterUpdate) onFilterUpdate(newFilterValue);
228
-
229
- if (column.key !== sortByProperty) {
230
- resetSort();
231
- }
232
- }, [checkFilterCombination, currentSort, onFilterUpdate, resetSort, sortByProperty]);
233
-
234
- const buildErrorView = useCallback(() => (
235
- <div
236
- className="h-full flex flex-col items-center justify-center sticky left-0">
237
-
238
- <Typography variant={"h6"}>
239
- {"Error fetching data from the data source"}
240
- </Typography>
241
-
242
- {error?.message && <Markdown className={"px-4 break-all"} source={error.message}/>}
243
-
244
- </div>
245
- ), [error?.message]);
228
+ }, [checkFilterCombination, currentSort, onFilterUpdate, sortByProperty]);
246
229
 
247
230
  const buildEmptyView = useCallback(() => {
248
231
  if (loading)
@@ -255,7 +238,18 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
255
238
  }, [emptyComponent, loading]);
256
239
 
257
240
  const empty = !loading && (data?.length ?? 0) === 0;
258
- const customView = error ? buildErrorView() : (empty ? buildEmptyView() : undefined);
241
+ const customView = error
242
+ ? <CenteredView maxWidth={"2xl"}
243
+ className="flex flex-col gap-2">
244
+
245
+ <Typography variant={"h6"}>
246
+ {"Error fetching data from the data source"}
247
+ </Typography>
248
+
249
+ {error?.message && <SafeLinkRenderer text={error.message}/>}
250
+
251
+ </CenteredView>
252
+ : (empty ? buildEmptyView() : undefined);
259
253
 
260
254
  const virtualListController = {
261
255
  data,
@@ -403,3 +397,17 @@ function MemoizedList({
403
397
  {Row}
404
398
  </List>;
405
399
  }
400
+
401
+ const SafeLinkRenderer: React.FC<{
402
+ text: string;
403
+ }> = ({ text }) => {
404
+ const urlRegex = /https?:\/\/[^\s]+/g;
405
+ const htmlContent = text.replace(urlRegex, (url) => {
406
+ // For each URL found, replace it with an HTML <a> tag
407
+ return `<a href="${url}" target="_blank">Link</a><br/>`;
408
+ });
409
+
410
+ return (
411
+ <div className={"break-all"} dangerouslySetInnerHTML={{ __html: htmlContent }}/>
412
+ );
413
+ };
@@ -36,7 +36,7 @@ export interface OnCellValueChangeParams<T = any, M extends Record<string, any>
36
36
  propertyKey: string,
37
37
  entity: Entity<M>,
38
38
  onValueUpdated: () => void
39
- setError: (e: Error) => void
39
+ setError: (e: Error | undefined) => void
40
40
  fullPath: string
41
41
  context: FireCMSContext
42
42
  }
@@ -12,8 +12,7 @@ import { DataSourceContext } from "../contexts/DataSourceContext";
12
12
  import { SideEntityControllerContext } from "../contexts/SideEntityControllerContext";
13
13
  import { NavigationContext } from "../contexts/NavigationContext";
14
14
  import { SideDialogsControllerContext } from "../contexts/SideDialogsControllerContext";
15
- import { useLocaleConfig } from "../internal/useLocaleConfig";
16
- import { CenteredView, Typography } from "@firecms/ui";
15
+ import { CenteredView, Typography, useLocaleConfig } from "@firecms/ui";
17
16
  import { DialogsProvider } from "../contexts/DialogsProvider";
18
17
  import { useBuildDataSource } from "../internal/useBuildDataSource";
19
18
  import { useBuildCustomizationController } from "../internal/useBuildCustomizationController";
@@ -359,8 +359,7 @@ export function getDefaultFieldId(property: Property | ResolvedProperty) {
359
359
  } else if (property.dataType === "map") {
360
360
  if (property.keyValue)
361
361
  return "key_value";
362
- if (property.properties)
363
- return "group";
362
+ return "group";
364
363
  } else if (property.dataType === "array") {
365
364
  const of = (property as ArrayProperty).of;
366
365
  const oneOf = (property as ArrayProperty).oneOf;
@@ -145,6 +145,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, Use
145
145
  onSaveSuccess(entity);
146
146
  })
147
147
  .catch((e) => {
148
+ console.error("!!!", e);
148
149
  if (callbacks?.onSaveFailure) {
149
150
 
150
151
  const resolvedCollection = resolveCollection<M>({