@firecms/core 3.0.0-beta.11 → 3.0.0-beta.13
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/app/Scaffold.d.ts +4 -0
- package/dist/components/ArrayContainer.d.ts +14 -3
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
- package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +15 -1
- package/dist/components/EntityCollectionTable/index.d.ts +1 -1
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
- package/dist/components/EntityJsonPreview.d.ts +3 -0
- package/dist/components/PropertyIdCopyTooltip.d.ts +1 -1
- package/dist/components/SelectableTable/SelectableTable.d.ts +12 -2
- package/dist/components/SelectableTable/filters/ReferenceFilterField.d.ts +1 -1
- package/dist/components/UnsavedChangesDialog.d.ts +8 -0
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +13 -0
- package/dist/components/common/default_entity_actions.d.ts +0 -2
- package/dist/components/common/index.d.ts +1 -1
- package/dist/components/common/useColumnsIds.d.ts +1 -0
- package/dist/components/common/{useDataSourceEntityCollectionTableController.d.ts → useDataSourceTableController.d.ts} +10 -2
- package/dist/components/common/useDebouncedCallback.d.ts +1 -0
- package/dist/components/common/useScrollRestoration.d.ts +14 -0
- package/dist/contexts/BreacrumbsContext.d.ts +8 -0
- package/dist/core/DefaultAppBar.d.ts +8 -2
- package/dist/core/EntityEditView.d.ts +36 -21
- package/dist/core/EntityEditViewFormActions.d.ts +2 -0
- package/dist/core/FireCMS.d.ts +2 -2
- package/dist/core/FireCMSRouter.d.ts +4 -0
- package/dist/core/NavigationRoutes.d.ts +0 -1
- package/dist/core/SideDialogs.d.ts +4 -2
- package/dist/core/index.d.ts +2 -1
- package/dist/form/EntityForm.d.ts +49 -0
- package/dist/form/EntityFormActions.d.ts +17 -0
- package/dist/form/PropertyFieldBinding.d.ts +1 -1
- package/dist/form/components/FormEntry.d.ts +6 -0
- package/dist/form/components/FormLayout.d.ts +5 -0
- package/dist/form/components/index.d.ts +2 -0
- package/dist/form/field_bindings/ArrayCustomShapedFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/ArrayOfReferencesFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/BlockFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/RepeatFieldBinding.d.ts +1 -1
- package/dist/form/index.d.ts +16 -16
- package/dist/hooks/data/save.d.ts +1 -1
- package/dist/hooks/data/useEntityFetch.d.ts +2 -1
- package/dist/hooks/useBreadcrumbsController.d.ts +26 -0
- package/dist/hooks/useBuildNavigationController.d.ts +4 -1
- package/dist/hooks/useModeController.d.ts +1 -2
- package/dist/index.es.js +15842 -14396
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +15833 -14389
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useBuildDataSource.d.ts +3 -2
- package/dist/internal/useBuildSideEntityController.d.ts +3 -3
- package/dist/internal/useUnsavedChangesDialog.d.ts +7 -9
- package/dist/routes/CustomCMSRoute.d.ts +4 -0
- package/dist/routes/FireCMSRoute.d.ts +1 -0
- package/dist/routes/HomePageRoute.d.ts +3 -0
- package/dist/types/collections.d.ts +36 -10
- package/dist/types/datasource.d.ts +2 -2
- package/dist/types/dialogs_controller.d.ts +7 -3
- package/dist/types/entities.d.ts +1 -1
- package/dist/types/entity_actions.d.ts +10 -0
- package/dist/types/fields.d.ts +13 -3
- package/dist/types/firecms.d.ts +1 -1
- package/dist/types/navigation.d.ts +33 -12
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/properties.d.ts +40 -4
- package/dist/types/side_entity_controller.d.ts +6 -1
- package/dist/util/builders.d.ts +1 -1
- package/dist/util/createFormexStub.d.ts +2 -0
- package/dist/util/entity_actions.d.ts +2 -0
- package/dist/util/entity_cache.d.ts +23 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/navigation_from_path.d.ts +6 -1
- package/dist/util/navigation_utils.d.ts +13 -1
- package/dist/util/objects.d.ts +1 -1
- package/dist/util/property_utils.d.ts +2 -2
- package/dist/util/references.d.ts +2 -2
- package/dist/util/resolutions.d.ts +12 -2
- package/package.json +20 -21
- package/src/app/Scaffold.tsx +13 -3
- package/src/components/ArrayContainer.tsx +60 -24
- package/src/components/CircularProgressCenter.tsx +1 -1
- package/src/components/DeleteEntityDialog.tsx +5 -3
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +46 -23
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +10 -4
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +18 -1
- package/src/components/EntityCollectionTable/index.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -3
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +10 -13
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +105 -47
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -2
- package/src/components/EntityJsonPreview.tsx +66 -0
- package/src/components/EntityPreview.tsx +10 -3
- package/src/components/EntityView.tsx +4 -1
- package/src/components/NotFoundPage.tsx +2 -2
- package/src/components/PropertyIdCopyTooltip.tsx +2 -3
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +14 -7
- package/src/components/SelectableTable/SelectableTable.tsx +18 -3
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +7 -7
- package/src/components/UnsavedChangesDialog.tsx +42 -0
- package/src/components/VirtualTable/VirtualTable.tsx +48 -3
- package/src/components/VirtualTable/VirtualTableProps.tsx +15 -0
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +1 -1
- package/src/components/common/default_entity_actions.tsx +37 -37
- package/src/components/common/index.ts +1 -1
- package/src/components/common/useColumnsIds.tsx +1 -1
- package/src/components/common/useDataSourceTableController.tsx +420 -0
- package/src/components/common/useDebouncedCallback.tsx +20 -0
- package/src/components/common/useScrollRestoration.tsx +68 -0
- package/src/contexts/BreacrumbsContext.tsx +38 -0
- package/src/contexts/DialogsProvider.tsx +3 -2
- package/src/contexts/ModeController.tsx +1 -3
- package/src/core/DefaultAppBar.tsx +59 -17
- package/src/core/DefaultDrawer.tsx +1 -1
- package/src/core/EntityEditView.tsx +354 -1000
- package/src/core/EntityEditViewFormActions.tsx +199 -0
- package/src/core/EntitySidePanel.tsx +85 -15
- package/src/core/FireCMS.tsx +20 -16
- package/src/core/FireCMSRouter.tsx +17 -0
- package/src/core/NavigationRoutes.tsx +23 -32
- package/src/core/SideDialogs.tsx +20 -11
- package/src/core/index.tsx +4 -2
- package/src/form/EntityForm.tsx +782 -0
- package/src/form/EntityFormActions.tsx +169 -0
- package/src/form/PropertyFieldBinding.tsx +21 -16
- package/src/form/components/FormEntry.tsx +22 -0
- package/src/form/components/FormLayout.tsx +16 -0
- package/src/form/components/LabelWithIcon.tsx +6 -3
- package/src/form/components/StorageUploadProgress.tsx +1 -1
- package/src/form/components/index.tsx +2 -0
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +8 -5
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +5 -2
- package/src/form/field_bindings/BlockFieldBinding.tsx +5 -3
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +1 -0
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +1 -1
- package/src/form/field_bindings/MapFieldBinding.tsx +14 -16
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +15 -8
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +3 -3
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -2
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
- package/src/form/field_bindings/RepeatFieldBinding.tsx +13 -4
- package/src/form/field_bindings/SelectFieldBinding.tsx +2 -2
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +11 -6
- package/src/form/field_bindings/SwitchFieldBinding.tsx +1 -0
- package/src/form/field_bindings/TextFieldBinding.tsx +1 -0
- package/src/form/index.tsx +16 -32
- package/src/form/validation.ts +12 -6
- package/src/hooks/data/save.ts +25 -30
- package/src/hooks/data/useCollectionFetch.tsx +1 -1
- package/src/hooks/data/useEntityFetch.tsx +5 -1
- package/src/hooks/useBreadcrumbsController.tsx +31 -0
- package/src/hooks/useBuildModeController.tsx +15 -28
- package/src/hooks/useBuildNavigationController.tsx +81 -24
- package/src/hooks/useFireCMSContext.tsx +0 -30
- package/src/hooks/useLargeLayout.tsx +0 -35
- package/src/hooks/useModeController.tsx +1 -2
- package/src/hooks/useResolvedNavigationFrom.tsx +4 -6
- package/src/internal/useBuildDataSource.ts +9 -5
- package/src/internal/useBuildSideDialogsController.tsx +3 -2
- package/src/internal/useBuildSideEntityController.tsx +135 -88
- package/src/internal/useUnsavedChangesDialog.tsx +126 -92
- package/src/preview/PropertyPreview.tsx +12 -10
- package/src/preview/components/UrlComponentPreview.tsx +17 -18
- package/src/preview/property_previews/ArrayOfMapsPreview.tsx +4 -3
- package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +4 -3
- package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +4 -2
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +4 -3
- package/src/preview/property_previews/ArrayOneOfPreview.tsx +4 -2
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +4 -2
- package/src/preview/property_previews/MapPropertyPreview.tsx +3 -2
- package/src/routes/CustomCMSRoute.tsx +21 -0
- package/src/routes/FireCMSRoute.tsx +246 -0
- package/src/routes/HomePageRoute.tsx +17 -0
- package/src/types/collections.ts +45 -13
- package/src/types/datasource.ts +2 -1
- package/src/types/dialogs_controller.tsx +7 -3
- package/src/types/entities.ts +1 -1
- package/src/types/entity_actions.tsx +12 -0
- package/src/types/fields.tsx +18 -3
- package/src/types/firecms.tsx +1 -1
- package/src/types/navigation.ts +41 -16
- package/src/types/plugins.tsx +2 -0
- package/src/types/properties.ts +48 -4
- package/src/types/side_dialogs_controller.tsx +2 -0
- package/src/types/side_entity_controller.tsx +11 -1
- package/src/util/builders.ts +5 -3
- package/src/util/createFormexStub.tsx +62 -0
- package/src/util/entity_actions.ts +28 -0
- package/src/util/entity_cache.ts +204 -0
- package/src/util/index.ts +1 -0
- package/src/util/join_collections.ts +6 -1
- package/src/util/navigation_from_path.ts +18 -7
- package/src/util/navigation_utils.ts +115 -14
- package/src/util/objects.ts +9 -2
- package/src/util/parent_references_from_path.ts +3 -3
- package/src/util/property_utils.tsx +7 -3
- package/src/util/references.ts +8 -6
- package/src/util/resolutions.ts +41 -12
- package/src/util/useStorageUploadController.tsx +60 -23
- package/src/components/common/useDataSourceEntityCollectionTableController.tsx +0 -236
- /package/src/util/{common.tsx → common.ts} +0 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { useDataSource, useFireCMSContext, useNavigationController } from "../../hooks";
|
|
4
|
+
import { useDataOrder } from "../../hooks/data/useDataOrder";
|
|
5
|
+
import {
|
|
6
|
+
Entity,
|
|
7
|
+
EntityCollection,
|
|
8
|
+
EntityReference,
|
|
9
|
+
EntityTableController,
|
|
10
|
+
FilterValues,
|
|
11
|
+
FireCMSContext,
|
|
12
|
+
SelectedCellProps,
|
|
13
|
+
User,
|
|
14
|
+
WhereFilterOp
|
|
15
|
+
} from "../../types";
|
|
16
|
+
import { useDebouncedData } from "./useDebouncedData";
|
|
17
|
+
import { ScrollRestorationController } from "./useScrollRestoration";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
20
|
+
|
|
21
|
+
export type DataSourceTableControllerProps<M extends Record<string, any> = any> = {
|
|
22
|
+
/**
|
|
23
|
+
* Full path where the data of this table is located
|
|
24
|
+
*/
|
|
25
|
+
fullPath: string;
|
|
26
|
+
/**
|
|
27
|
+
* The collection that is represented by this config.
|
|
28
|
+
*/
|
|
29
|
+
collection: EntityCollection<M>;
|
|
30
|
+
/**
|
|
31
|
+
* List of entities that will be displayed on top, no matter the ordering.
|
|
32
|
+
* This is used for reference fields selection
|
|
33
|
+
*/
|
|
34
|
+
entitiesDisplayedFirst?: Entity<M>[];
|
|
35
|
+
|
|
36
|
+
lastDeleteTimestamp?: number;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Force filter to be applied to the table.
|
|
40
|
+
*/
|
|
41
|
+
forceFilter?: FilterValues<string>;
|
|
42
|
+
|
|
43
|
+
scrollRestoration?: ScrollRestorationController;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* When set to true the filters and sort will be updated in the URL
|
|
47
|
+
*/
|
|
48
|
+
updateUrl?: boolean;
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Use this hook to build a controller for the {@link EntityCollectionTable}.
|
|
54
|
+
* This controller is bound to data in a path in your specified datasource.
|
|
55
|
+
*
|
|
56
|
+
* Note that you can build your own hook returning a {@link EntityTableController}
|
|
57
|
+
* if you would like to display different data.
|
|
58
|
+
*
|
|
59
|
+
* @param fullPath
|
|
60
|
+
* @param collection
|
|
61
|
+
* @param scrollRestoration
|
|
62
|
+
* @param entitiesDisplayedFirst
|
|
63
|
+
* @param lastDeleteTimestamp
|
|
64
|
+
* @param forceFilterFromProps
|
|
65
|
+
* @param updateUrl
|
|
66
|
+
*/
|
|
67
|
+
export function useDataSourceTableController<M extends Record<string, any> = any, USER extends User = User>(
|
|
68
|
+
{
|
|
69
|
+
fullPath,
|
|
70
|
+
collection,
|
|
71
|
+
scrollRestoration,
|
|
72
|
+
entitiesDisplayedFirst,
|
|
73
|
+
lastDeleteTimestamp,
|
|
74
|
+
forceFilter: forceFilterFromProps,
|
|
75
|
+
updateUrl
|
|
76
|
+
}: DataSourceTableControllerProps<M>)
|
|
77
|
+
: EntityTableController<M> {
|
|
78
|
+
|
|
79
|
+
const {
|
|
80
|
+
initialFilter,
|
|
81
|
+
initialSort,
|
|
82
|
+
forceFilter: forceFilterFromCollection
|
|
83
|
+
} = collection;
|
|
84
|
+
|
|
85
|
+
const [popupCell, setPopupCell] = React.useState<SelectedCellProps<M> | undefined>(undefined);
|
|
86
|
+
const navigation = useNavigationController();
|
|
87
|
+
const dataSource = useDataSource(collection);
|
|
88
|
+
const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
|
|
89
|
+
|
|
90
|
+
const forceFilter = forceFilterFromProps ?? forceFilterFromCollection;
|
|
91
|
+
const paginationEnabled = collection.pagination === undefined || Boolean(collection.pagination);
|
|
92
|
+
const pageSize = typeof collection.pagination === "number" ? collection.pagination : DEFAULT_PAGE_SIZE;
|
|
93
|
+
|
|
94
|
+
const [searchString, setSearchString] = React.useState<string | undefined>();
|
|
95
|
+
|
|
96
|
+
const checkFilterCombination = useCallback((filterValues: FilterValues<any>,
|
|
97
|
+
sortBy?: [string, "asc" | "desc"]) => {
|
|
98
|
+
if (!dataSource.isFilterCombinationValid)
|
|
99
|
+
return true;
|
|
100
|
+
return dataSource.isFilterCombinationValid({
|
|
101
|
+
path: resolvedPath,
|
|
102
|
+
collection,
|
|
103
|
+
filterValues,
|
|
104
|
+
sortBy
|
|
105
|
+
})
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const onScroll = ({
|
|
109
|
+
scrollOffset
|
|
110
|
+
}: {
|
|
111
|
+
scrollOffset: number
|
|
112
|
+
}) => {
|
|
113
|
+
if (scrollRestoration) {
|
|
114
|
+
scrollRestoration.updateCollectionScroll({
|
|
115
|
+
fullPath: resolvedPath,
|
|
116
|
+
scrollOffset,
|
|
117
|
+
data: rawData,
|
|
118
|
+
filters: filterValues
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const initialSortInternal = useMemo(() => {
|
|
124
|
+
if (initialSort && forceFilter && !checkFilterCombination(forceFilter, initialSort)) {
|
|
125
|
+
console.warn("Initial sort is not compatible with the force filter. Ignoring initial sort");
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return initialSort;
|
|
129
|
+
}, [initialSort, forceFilter]);
|
|
130
|
+
|
|
131
|
+
const {
|
|
132
|
+
filterValues: initialFilterUrl,
|
|
133
|
+
sortBy: initialSortUrl,
|
|
134
|
+
} = parseFilterAndSort(window.location.search);
|
|
135
|
+
|
|
136
|
+
const [filterValues, setFilterValues] = React.useState<FilterValues<Extract<keyof M, string>> | undefined>(forceFilter ?? (updateUrl ? initialFilterUrl : undefined) ?? initialFilter ?? undefined);
|
|
137
|
+
const [sortBy, setSortBy] = React.useState<[Extract<keyof M, string>, "asc" | "desc"] | undefined>(initialSortUrl ?? initialSortInternal);
|
|
138
|
+
|
|
139
|
+
useUpdateUrl(filterValues, sortBy, searchString, updateUrl);
|
|
140
|
+
|
|
141
|
+
const collectionScroll = scrollRestoration?.getCollectionScroll(fullPath, filterValues);
|
|
142
|
+
const initialItemCount = collectionScroll?.data.length ?? pageSize;
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (scrollRestoration) {
|
|
146
|
+
scrollRestoration.updateCollectionScroll({
|
|
147
|
+
fullPath: resolvedPath,
|
|
148
|
+
scrollOffset: collectionScroll?.scrollOffset ?? 0,
|
|
149
|
+
data: rawData,
|
|
150
|
+
filters: filterValues
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
const [itemCount, setItemCount] = React.useState<number | undefined>(paginationEnabled ? initialItemCount : undefined);
|
|
156
|
+
|
|
157
|
+
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
158
|
+
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
159
|
+
|
|
160
|
+
const context: FireCMSContext<USER> = useFireCMSContext();
|
|
161
|
+
|
|
162
|
+
const [rawData, setRawData] = useState<Entity<M>[]>(collectionScroll?.data ?? []);
|
|
163
|
+
|
|
164
|
+
const [dataLoading, setDataLoading] = useState<boolean>(false);
|
|
165
|
+
const [dataLoadingError, setDataLoadingError] = useState<Error | undefined>();
|
|
166
|
+
const [noMoreToLoad, setNoMoreToLoad] = useState<boolean>(false);
|
|
167
|
+
|
|
168
|
+
const clearFilter = useCallback(() => setFilterValues(forceFilter ?? undefined), [forceFilter]);
|
|
169
|
+
|
|
170
|
+
const updateFilterValues = useCallback((updatedFilter: FilterValues<Extract<keyof M, string>> | undefined) => {
|
|
171
|
+
if (forceFilter) {
|
|
172
|
+
console.warn("Filter is not compatible with the force filter. Ignoring filter");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (updatedFilter && Object.keys(updatedFilter).length === 0) {
|
|
176
|
+
setFilterValues(undefined);
|
|
177
|
+
} else {
|
|
178
|
+
setFilterValues(updatedFilter);
|
|
179
|
+
}
|
|
180
|
+
}, [forceFilter]);
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
|
|
184
|
+
setDataLoading(true);
|
|
185
|
+
|
|
186
|
+
const onEntitiesUpdate = async (entities: Entity<M>[]) => {
|
|
187
|
+
if (collection.callbacks?.onFetch) {
|
|
188
|
+
try {
|
|
189
|
+
entities = await Promise.all(
|
|
190
|
+
entities.map((entity) =>
|
|
191
|
+
collection.callbacks!.onFetch!({
|
|
192
|
+
collection,
|
|
193
|
+
path: resolvedPath,
|
|
194
|
+
entity,
|
|
195
|
+
context
|
|
196
|
+
})));
|
|
197
|
+
} catch (e: any) {
|
|
198
|
+
console.error(e);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
setDataLoading(false);
|
|
202
|
+
setDataLoadingError(undefined);
|
|
203
|
+
setRawData(entities.map(e => ({
|
|
204
|
+
...e,
|
|
205
|
+
// values: sanitizeData(e.values, resolvedCollection.properties)
|
|
206
|
+
})));
|
|
207
|
+
setNoMoreToLoad(!itemCount || entities.length < itemCount);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const onError = (error: Error) => {
|
|
211
|
+
console.error("ERROR", error);
|
|
212
|
+
setDataLoading(false);
|
|
213
|
+
setRawData([]);
|
|
214
|
+
setDataLoadingError(error);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (dataSource.listenCollection) {
|
|
218
|
+
return dataSource.listenCollection<M>({
|
|
219
|
+
path: resolvedPath,
|
|
220
|
+
collection,
|
|
221
|
+
onUpdate: onEntitiesUpdate,
|
|
222
|
+
onError,
|
|
223
|
+
searchString,
|
|
224
|
+
filter: filterValues,
|
|
225
|
+
limit: itemCount,
|
|
226
|
+
startAfter: undefined,
|
|
227
|
+
orderBy: sortByProperty,
|
|
228
|
+
order: currentSort
|
|
229
|
+
});
|
|
230
|
+
} else {
|
|
231
|
+
dataSource.fetchCollection<M>({
|
|
232
|
+
path: resolvedPath,
|
|
233
|
+
collection,
|
|
234
|
+
searchString,
|
|
235
|
+
filter: filterValues,
|
|
236
|
+
limit: itemCount,
|
|
237
|
+
startAfter: undefined,
|
|
238
|
+
orderBy: sortByProperty,
|
|
239
|
+
order: currentSort
|
|
240
|
+
})
|
|
241
|
+
.then(onEntitiesUpdate)
|
|
242
|
+
.catch(onError);
|
|
243
|
+
return () => {
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}, [resolvedPath, itemCount, currentSort, sortByProperty, filterValues, searchString]);
|
|
247
|
+
|
|
248
|
+
const orderedData = useDataOrder({
|
|
249
|
+
data: rawData,
|
|
250
|
+
entitiesDisplayedFirst
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// hack to fix Firestore listeners firing with incomplete data
|
|
254
|
+
const data = useDebouncedData(orderedData, {
|
|
255
|
+
filterValues,
|
|
256
|
+
sortBy,
|
|
257
|
+
searchString,
|
|
258
|
+
lastDeleteTimestamp
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
data,
|
|
263
|
+
dataLoading,
|
|
264
|
+
noMoreToLoad,
|
|
265
|
+
dataLoadingError,
|
|
266
|
+
filterValues,
|
|
267
|
+
setFilterValues: updateFilterValues,
|
|
268
|
+
sortBy,
|
|
269
|
+
setSortBy,
|
|
270
|
+
searchString,
|
|
271
|
+
setSearchString,
|
|
272
|
+
clearFilter,
|
|
273
|
+
itemCount,
|
|
274
|
+
setItemCount,
|
|
275
|
+
initialScroll: collectionScroll?.scrollOffset,
|
|
276
|
+
onScroll,
|
|
277
|
+
paginationEnabled,
|
|
278
|
+
pageSize,
|
|
279
|
+
checkFilterCombination,
|
|
280
|
+
popupCell,
|
|
281
|
+
setPopupCell
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function useUpdateUrl<M extends Record<string, any> = any>(
|
|
286
|
+
filterValues: FilterValues<Extract<keyof M, string>> | undefined,
|
|
287
|
+
sortBy: [Extract<keyof M, string>, "asc" | "desc"] | undefined,
|
|
288
|
+
searchString: string | undefined,
|
|
289
|
+
updateUrl: boolean | undefined
|
|
290
|
+
) {
|
|
291
|
+
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (updateUrl) {
|
|
294
|
+
const newUrl = encodeFilterAndSort(filterValues, sortBy);
|
|
295
|
+
const search = searchString ? `&search=${encodeURIComponent(searchString)}` : "";
|
|
296
|
+
const state = `${newUrl}${search}`;
|
|
297
|
+
const hash = window.location.hash;
|
|
298
|
+
if (state === "")
|
|
299
|
+
window.history.replaceState({}, "", `${window.location.pathname}${hash}`);
|
|
300
|
+
else
|
|
301
|
+
window.history.replaceState({}, "", `?${state}${hash}`);
|
|
302
|
+
}
|
|
303
|
+
}, [filterValues, sortBy, searchString, updateUrl]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function encodeFilterAndSort(filterValues?: FilterValues<string>, sortBy?: [string, "asc" | "desc"] | undefined) {
|
|
307
|
+
const entries: Record<string, string> = {};
|
|
308
|
+
if (sortBy) {
|
|
309
|
+
entries["__sort"] = encodeURIComponent(sortBy[0]);
|
|
310
|
+
entries["__sort_order"] = encodeURIComponent(sortBy[1]);
|
|
311
|
+
}
|
|
312
|
+
if (filterValues) {
|
|
313
|
+
Object.entries(filterValues).forEach(([key, value]) => {
|
|
314
|
+
if (value) {
|
|
315
|
+
const [op, val] = value;
|
|
316
|
+
let encodedValue: any = val;
|
|
317
|
+
try {
|
|
318
|
+
if (typeof val === "object") {
|
|
319
|
+
if (val instanceof Date) {
|
|
320
|
+
encodedValue = val.toISOString();
|
|
321
|
+
} else if (Array.isArray(val)) {
|
|
322
|
+
encodedValue = JSON.stringify(val, (key, value) => {
|
|
323
|
+
if (value instanceof EntityReference) {
|
|
324
|
+
return encodeRef(value);
|
|
325
|
+
}
|
|
326
|
+
return value;
|
|
327
|
+
});
|
|
328
|
+
} else if (val instanceof EntityReference) {
|
|
329
|
+
encodedValue = encodeRef(val);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} catch (e) {
|
|
333
|
+
encodedValue = val;
|
|
334
|
+
}
|
|
335
|
+
if (encodedValue !== undefined) {
|
|
336
|
+
entries[encodeURIComponent(`${key}_op`)] = encodeURIComponent(op);
|
|
337
|
+
entries[encodeURIComponent(`${key}_value`)] = encodeURIComponent(encodedValue.toString());
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (!Object.keys(entries).length) {
|
|
343
|
+
return "";
|
|
344
|
+
}
|
|
345
|
+
return Object.entries(entries).map(([key, value]) => `${key}=${value}`).join("&");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseFilterAndSort<M>(search: string): {
|
|
349
|
+
filterValues: FilterValues<string> | undefined,
|
|
350
|
+
sortBy?: [Extract<keyof M, string>, "asc" | "desc"]
|
|
351
|
+
} {
|
|
352
|
+
const entries = new URLSearchParams(search);
|
|
353
|
+
const filterValues: FilterValues<string> = {};
|
|
354
|
+
let sortBy: [string, "asc" | "desc"] | undefined = undefined;
|
|
355
|
+
entries.forEach((value, key) => {
|
|
356
|
+
if (key === "__sort") {
|
|
357
|
+
sortBy = [decodeURIComponent(value), entries.get("__sort_order") as "asc" | "desc"];
|
|
358
|
+
} else if (key.endsWith("_op")) {
|
|
359
|
+
const field = key.replace("_op", "");
|
|
360
|
+
const filterOp = decodeURIComponent(value) as WhereFilterOp;
|
|
361
|
+
const filterValStr = entries.get(`${field}_value`);
|
|
362
|
+
if (filterValStr !== null) {
|
|
363
|
+
filterValues[field] = [filterOp, decodeString(filterValStr)];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
filterValues: Object.keys(filterValues).length ? filterValues : undefined,
|
|
370
|
+
sortBy
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function isDate(dateString: string): boolean {
|
|
375
|
+
// Define a regex pattern that matches the exact date format: 2025-01-07T23:00:00.000Z
|
|
376
|
+
const regexPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
377
|
+
|
|
378
|
+
// Test the dateString against the regex pattern
|
|
379
|
+
if (!regexPattern.test(dateString)) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// If the regex matches, further validate if it is a valid UTC date
|
|
384
|
+
const date = new Date(dateString);
|
|
385
|
+
return date.toISOString() === dateString;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function encodeRef(val: EntityReference) {
|
|
389
|
+
return `ref::${val.path}/${val.id}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function decodeString(val: string): EntityReference | Date | string {
|
|
393
|
+
let parsedFilterVal: any = val;
|
|
394
|
+
if (isDate(val)) {
|
|
395
|
+
try {
|
|
396
|
+
parsedFilterVal = new Date(val);
|
|
397
|
+
} catch (e) {
|
|
398
|
+
// ignore
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (typeof parsedFilterVal === "string") {
|
|
402
|
+
try {
|
|
403
|
+
parsedFilterVal = JSON.parse(parsedFilterVal, (key, value) => {
|
|
404
|
+
if (typeof value === "string" && value.startsWith("ref::")) {
|
|
405
|
+
const [path, id] = value.substring(5).split("/");
|
|
406
|
+
return new EntityReference(id, path);
|
|
407
|
+
}
|
|
408
|
+
return value;
|
|
409
|
+
});
|
|
410
|
+
} catch (e) {
|
|
411
|
+
// ignore
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (typeof parsedFilterVal === "string" && parsedFilterVal.startsWith("ref::")) {
|
|
416
|
+
const [path, id] = parsedFilterVal.substring(5).split("/");
|
|
417
|
+
return new EntityReference(id, path);
|
|
418
|
+
}
|
|
419
|
+
return parsedFilterVal;
|
|
420
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export function useDebounceCallback<T extends (...args: any[]) => any>(
|
|
4
|
+
callback?: T,
|
|
5
|
+
delay?: number
|
|
6
|
+
): T {
|
|
7
|
+
const timeoutRef = useRef<number | null>(null);
|
|
8
|
+
|
|
9
|
+
const debouncedCallback = useCallback((...args: Parameters<T>) => {
|
|
10
|
+
if (timeoutRef.current !== null) {
|
|
11
|
+
clearTimeout(timeoutRef.current);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
15
|
+
callback?.(...args);
|
|
16
|
+
}, delay ?? 200);
|
|
17
|
+
}, [callback, delay]);
|
|
18
|
+
|
|
19
|
+
return debouncedCallback as T;
|
|
20
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Entity, FilterValues } from "../../types";
|
|
2
|
+
|
|
3
|
+
const collectionScrollCache = new Map<string, { scrollOffset: number, data: Entity<any>[] }>();
|
|
4
|
+
|
|
5
|
+
export type ScrollRestorationController = {
|
|
6
|
+
|
|
7
|
+
getCollectionScroll: (fullPath: string,
|
|
8
|
+
filters?: FilterValues<any>) => {
|
|
9
|
+
scrollOffset: number,
|
|
10
|
+
data: Entity<any>[]
|
|
11
|
+
} | undefined;
|
|
12
|
+
|
|
13
|
+
updateCollectionScroll: (props: {
|
|
14
|
+
fullPath: string,
|
|
15
|
+
scrollOffset: number,
|
|
16
|
+
filters?: FilterValues<any>;
|
|
17
|
+
data: Entity<any>[]
|
|
18
|
+
}) => void;
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useScrollRestoration(): ScrollRestorationController {
|
|
23
|
+
|
|
24
|
+
const updateCollectionScroll = ({
|
|
25
|
+
fullPath,
|
|
26
|
+
filters,
|
|
27
|
+
scrollOffset,
|
|
28
|
+
data
|
|
29
|
+
}: {
|
|
30
|
+
fullPath: string;
|
|
31
|
+
filters?: FilterValues<any>;
|
|
32
|
+
sort?: [string, "asc" | "desc"];
|
|
33
|
+
scrollOffset: number;
|
|
34
|
+
data: Entity<any>[]
|
|
35
|
+
}) => {
|
|
36
|
+
collectionScrollCache.set(
|
|
37
|
+
createCacheKey(fullPath, filters),
|
|
38
|
+
{
|
|
39
|
+
scrollOffset,
|
|
40
|
+
data
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const getCollectionScroll = (fullPath: string,
|
|
45
|
+
filters?: FilterValues<any>) => {
|
|
46
|
+
return collectionScrollCache.get(createCacheKey(fullPath, filters));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
getCollectionScroll,
|
|
51
|
+
updateCollectionScroll
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createCacheKey(fullPath: string, filters?: FilterValues<any>) {
|
|
56
|
+
|
|
57
|
+
if (!filters) {
|
|
58
|
+
return fullPath;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// codify the filters into a url friendly string
|
|
62
|
+
const filtersString = filters ? Object.keys(filters).map(key => {
|
|
63
|
+
const value = JSON.stringify(filters[key]);
|
|
64
|
+
return `${key}=${value}`;
|
|
65
|
+
}).join("&") : "";
|
|
66
|
+
|
|
67
|
+
return `${fullPath}?${filtersString}`;
|
|
68
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { BreadcrumbEntry, BreadcrumbsController } from "../hooks/useBreadcrumbsController";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_BREADCRUMBS_CONTROLLER = {
|
|
5
|
+
breadcrumbs: [],
|
|
6
|
+
set: (props: {
|
|
7
|
+
breadcrumbs: BreadcrumbEntry[];
|
|
8
|
+
}) => {
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const BreadcrumbContext = React.createContext<BreadcrumbsController>(DEFAULT_BREADCRUMBS_CONTROLLER);
|
|
13
|
+
|
|
14
|
+
interface BreadcrumbsProviderProps {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const BreadcrumbsProvider: React.FC<BreadcrumbsProviderProps> = ({ children }) => {
|
|
19
|
+
|
|
20
|
+
const [breadcrumbs, setBreadcrumbs] = useState<BreadcrumbEntry[]>([]);
|
|
21
|
+
|
|
22
|
+
const set = (props: {
|
|
23
|
+
breadcrumbs: BreadcrumbEntry[];
|
|
24
|
+
}) => {
|
|
25
|
+
setBreadcrumbs(props.breadcrumbs);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<BreadcrumbContext.Provider
|
|
30
|
+
value={{
|
|
31
|
+
breadcrumbs,
|
|
32
|
+
set
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</BreadcrumbContext.Provider>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -8,7 +8,7 @@ export const DialogsProvider: React.FC<PropsWithChildren<{}>> = ({ children }) =
|
|
|
8
8
|
const [dialogEntries, setDialogEntries] = useState<DialogControllerEntryProps[]>([]);
|
|
9
9
|
const dialogEntriesRef = useRef<DialogControllerEntryProps[]>(dialogEntries);
|
|
10
10
|
|
|
11
|
-
const updateDialogEntries = (newPanels: DialogControllerEntryProps[]) => {
|
|
11
|
+
const updateDialogEntries = (newPanels: DialogControllerEntryProps<any>[]) => {
|
|
12
12
|
dialogEntriesRef.current = newPanels;
|
|
13
13
|
setDialogEntries(newPanels);
|
|
14
14
|
};
|
|
@@ -23,7 +23,7 @@ export const DialogsProvider: React.FC<PropsWithChildren<{}>> = ({ children }) =
|
|
|
23
23
|
|
|
24
24
|
}, [dialogEntries]);
|
|
25
25
|
|
|
26
|
-
const open = useCallback((dialogEntry: DialogControllerEntryProps) => {
|
|
26
|
+
const open = useCallback(<T extends object = object>(dialogEntry: DialogControllerEntryProps<T>) => {
|
|
27
27
|
|
|
28
28
|
const updatedPanels = [...dialogEntriesRef.current, dialogEntry];
|
|
29
29
|
updateDialogEntries(updatedPanels);
|
|
@@ -46,6 +46,7 @@ export const DialogsProvider: React.FC<PropsWithChildren<{}>> = ({ children }) =
|
|
|
46
46
|
key={`dialog_${i}`}
|
|
47
47
|
open={true}
|
|
48
48
|
closeDialog={close}
|
|
49
|
+
{...entry.props}
|
|
49
50
|
/>)}
|
|
50
51
|
</DialogsControllerContext.Provider>
|
|
51
52
|
);
|
|
@@ -3,10 +3,8 @@ import { ModeController } from "../hooks";
|
|
|
3
3
|
|
|
4
4
|
const DEFAULT_MODE_STATE: ModeController = {
|
|
5
5
|
mode: "light",
|
|
6
|
-
setMode: (mode: "light" | "dark") => {
|
|
6
|
+
setMode: (mode: "light" | "dark" | "system") => {
|
|
7
7
|
},
|
|
8
|
-
toggleMode: () => {
|
|
9
|
-
}
|
|
10
8
|
};
|
|
11
9
|
export const ModeControllerContext = React.createContext<ModeController>(DEFAULT_MODE_STATE);
|
|
12
10
|
|