@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.
- package/dist/components/ClearFilterSortButton.d.ts +5 -0
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +11 -11
- package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +1 -1
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +1 -4
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +11 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +11 -0
- package/dist/components/SelectableTable/SelectableTable.d.ts +1 -1
- package/dist/components/common/types.d.ts +1 -1
- package/dist/form/components/ErrorFocus.d.ts +1 -1
- package/dist/hooks/useValidateAuthenticator.d.ts +2 -6
- package/dist/index.es.js +3516 -3381
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +5 -5
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useBuildDataSource.d.ts +1 -16
- package/dist/types/collections.d.ts +5 -1
- package/dist/types/datasource.d.ts +2 -5
- package/dist/types/entities.d.ts +5 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/navigation.d.ts +1 -1
- package/dist/types/plugins.d.ts +3 -1
- package/dist/types/properties.d.ts +2 -2
- package/dist/util/entities.d.ts +1 -1
- package/dist/util/navigation_utils.d.ts +2 -2
- package/dist/util/resolutions.d.ts +5 -5
- package/package.json +22 -19
- package/src/components/ClearFilterSortButton.tsx +41 -0
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +10 -11
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +1 -1
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +16 -19
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +27 -32
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +11 -6
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +28 -5
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +68 -0
- package/src/components/EntityView.tsx +1 -1
- package/src/components/HomePage/DefaultHomePage.tsx +1 -1
- package/src/components/SelectableTable/SelectableTable.tsx +1 -1
- package/src/components/SelectableTable/filters/BooleanFilterField.tsx +2 -3
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +22 -7
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +24 -5
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +35 -15
- package/src/components/VirtualTable/VirtualTable.tsx +28 -20
- package/src/components/common/types.tsx +1 -1
- package/src/core/FireCMS.tsx +1 -2
- package/src/core/field_configs.tsx +1 -2
- package/src/hooks/data/save.ts +1 -0
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +9 -10
- package/src/hooks/useBuildModeController.tsx +11 -5
- package/src/hooks/useBuildNavigationController.tsx +76 -19
- package/src/hooks/useProjectLog.tsx +3 -3
- package/src/hooks/useValidateAuthenticator.tsx +2 -22
- package/src/internal/useBuildDataSource.ts +42 -47
- package/src/internal/useBuildSideEntityController.tsx +16 -13
- package/src/preview/PropertyPreview.tsx +2 -12
- package/src/types/collections.ts +5 -1
- package/src/types/datasource.ts +7 -4
- package/src/types/entities.ts +9 -1
- package/src/types/index.ts +0 -1
- package/src/types/navigation.ts +1 -1
- package/src/types/plugins.tsx +4 -3
- package/src/types/properties.ts +5 -4
- package/src/util/entities.ts +1 -1
- package/src/util/navigation_utils.ts +6 -6
- package/dist/internal/useLocaleConfig.d.ts +0 -1
- package/dist/types/appcheck.d.ts +0 -26
- package/src/internal/useLocaleConfig.tsx +0 -18
- 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
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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 =
|
|
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
|
|
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-[
|
|
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]
|
|
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-[
|
|
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]
|
|
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
|
-
|
|
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-
|
|
132
|
+
className="absolute right-2 top-3"
|
|
133
133
|
onClick={(e) => updateFilter(operation, undefined)}>
|
|
134
134
|
<ClearIcon/>
|
|
135
135
|
</IconButton>}
|
|
136
|
-
renderValue={(enumKey) =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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,
|
|
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
|
|
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
|
}
|
package/src/core/FireCMS.tsx
CHANGED
|
@@ -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 "
|
|
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
|
-
|
|
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;
|
package/src/hooks/data/save.ts
CHANGED
|
@@ -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>({
|