@rebasepro/app 0.17.3 → 0.18.1
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/README.md +4 -1
- package/dist/components/index.d.ts +0 -3
- package/dist/contexts/AuthControllerContext.d.ts +2 -1
- package/dist/contexts/CustomizationControllerContext.d.ts +2 -1
- package/dist/contexts/DialogsProvider.d.ts +2 -1
- package/dist/contexts/RebaseDataContext.d.ts +10 -1
- package/dist/contexts/StorageSourceContext.d.ts +11 -1
- package/dist/core/RebaseProps.d.ts +2 -12
- package/dist/debug/index.d.ts +19 -0
- package/dist/debug.js +3631 -0
- package/dist/hooks/data/delete.d.ts +16 -7
- package/dist/hooks/data/save.d.ts +18 -4
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useAdminModeController.d.ts +5 -4
- package/dist/hooks/useNavigationGroupLabel.d.ts +17 -0
- package/dist/hooks/useStudioBridge.d.ts +33 -0
- package/dist/index.es.js +2599 -6980
- package/dist/index.es.js.map +1 -1
- package/dist/vitePlugin.d.ts +22 -0
- package/dist/vitePlugin.js +40 -1
- package/package.json +42 -31
- package/src/auth/api.ts +0 -134
- package/src/auth/index.ts +0 -26
- package/src/auth/types.ts +0 -115
- package/src/auth/useRebaseAuthController.ts +0 -345
- package/src/collections/collection_view_config.ts +0 -76
- package/src/collections/entity-display-cache.ts +0 -195
- package/src/collections/entity-display.ts +0 -77
- package/src/collections/entity_image_preview.ts +0 -61
- package/src/collections/filter-operator-resolution.ts +0 -119
- package/src/collections/form-layout.ts +0 -394
- package/src/collections/index.ts +0 -13
- package/src/collections/navigation_from_path.ts +0 -123
- package/src/collections/navigation_utils.ts +0 -171
- package/src/collections/parent_references_from_path.ts +0 -59
- package/src/collections/property-path.ts +0 -30
- package/src/collections/property_presentation.ts +0 -264
- package/src/collections/summary-property.ts +0 -114
- package/src/collections/title-property.ts +0 -312
- package/src/components/AIIcon.tsx +0 -47
- package/src/components/AuthSimulationMenu.tsx +0 -0
- package/src/components/ConfirmationDialog.tsx +0 -48
- package/src/components/Debug/UIReferenceView.tsx +0 -1053
- package/src/components/Debug/UIStyleGuide.tsx +0 -159
- package/src/components/Debug/collection-views/CardViewDemo.tsx +0 -87
- package/src/components/Debug/collection-views/CollectionTableDemo.tsx +0 -107
- package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +0 -89
- package/src/components/Debug/collection-views/index.ts +0 -3
- package/src/components/Debug/collection-views/sample_data.ts +0 -44
- package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +0 -387
- package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +0 -1100
- package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +0 -131
- package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +0 -288
- package/src/components/Debug/crm-dashboard/RecentActivity.tsx +0 -213
- package/src/components/Debug/crm-dashboard/TaskFilters.tsx +0 -182
- package/src/components/Debug/crm-dashboard/TaskTable.tsx +0 -425
- package/src/components/Debug/crm-dashboard/TasksView.tsx +0 -234
- package/src/components/ErrorTooltip.tsx +0 -12
- package/src/components/ErrorView.tsx +0 -94
- package/src/components/LanguageToggle.tsx +0 -59
- package/src/components/LoginView/LoginView.tsx +0 -1235
- package/src/components/LoginView/index.ts +0 -2
- package/src/components/LoginView/oauth-redirect-flow.ts +0 -184
- package/src/components/NotFoundPage.tsx +0 -27
- package/src/components/RebaseAuth.tsx +0 -27
- package/src/components/RebaseLogo.tsx +0 -88
- package/src/components/SchemaDriftBanner.tsx +0 -102
- package/src/components/UnsavedChangesDialog.tsx +0 -46
- package/src/components/UserDisplay.tsx +0 -54
- package/src/components/UserSelectPopover.tsx +0 -399
- package/src/components/UserSettingsView.tsx +0 -318
- package/src/components/common/index.ts +0 -6
- package/src/components/common/table_height.tsx +0 -21
- package/src/components/common/types.tsx +0 -66
- package/src/components/common/useColumnsIds.tsx +0 -261
- package/src/components/common/useDataTableController.tsx +0 -571
- package/src/components/common/useDebouncedData.ts +0 -49
- package/src/components/common/useScrollRestoration.tsx +0 -68
- package/src/components/index.tsx +0 -26
- package/src/contexts/AdminModeController.tsx +0 -11
- package/src/contexts/AnalyticsContext.tsx +0 -4
- package/src/contexts/AuthControllerContext.tsx +0 -4
- package/src/contexts/CollectionResolverContext.tsx +0 -24
- package/src/contexts/CollectionScopeContext.tsx +0 -68
- package/src/contexts/ComponentOverrideContext.tsx +0 -81
- package/src/contexts/CustomizationControllerContext.tsx +0 -4
- package/src/contexts/DataDriverContext.tsx +0 -4
- package/src/contexts/DataSourcesContext.tsx +0 -36
- package/src/contexts/DatabaseAdminContext.tsx +0 -4
- package/src/contexts/DialogsProvider.tsx +0 -56
- package/src/contexts/EffectiveRoleController.tsx +0 -12
- package/src/contexts/ModeController.tsx +0 -11
- package/src/contexts/RebaseClientInstanceContext.tsx +0 -7
- package/src/contexts/RebaseDataContext.tsx +0 -4
- package/src/contexts/SnackbarProvider.tsx +0 -15
- package/src/contexts/StorageSourceContext.tsx +0 -4
- package/src/contexts/StorageSourcesContext.tsx +0 -30
- package/src/contexts/UserConfigurationPersistenceContext.tsx +0 -4
- package/src/contexts/index.ts +0 -18
- package/src/core/PluginLifecycleManager.tsx +0 -96
- package/src/core/PluginProviderStack.tsx +0 -40
- package/src/core/Rebase.tsx +0 -488
- package/src/core/RebaseProps.tsx +0 -333
- package/src/core/RebaseRouter.tsx +0 -18
- package/src/core/RebaseRoutes.tsx +0 -24
- package/src/core/index.tsx +0 -5
- package/src/hooks/ApiConfigContext.tsx +0 -83
- package/src/hooks/data/collectionQuery.ts +0 -55
- package/src/hooks/data/delete.ts +0 -56
- package/src/hooks/data/save.ts +0 -78
- package/src/hooks/data/useCollection.tsx +0 -183
- package/src/hooks/data/useData.tsx +0 -18
- package/src/hooks/data/useDataOrder.ts +0 -26
- package/src/hooks/data/useFetch.tsx +0 -156
- package/src/hooks/data/useRelationSelector.tsx +0 -280
- package/src/hooks/index.tsx +0 -45
- package/src/hooks/useAdminModeController.tsx +0 -23
- package/src/hooks/useAnalyticsController.tsx +0 -8
- package/src/hooks/useAuthController.tsx +0 -15
- package/src/hooks/useAuthSubscription.ts +0 -130
- package/src/hooks/useBackendStorageSource.ts +0 -295
- package/src/hooks/useBridgeRegistration.tsx +0 -32
- package/src/hooks/useBrowserTitleAndIcon.tsx +0 -23
- package/src/hooks/useBuildAdminModeController.tsx +0 -29
- package/src/hooks/useBuildEffectiveRoleController.tsx +0 -30
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +0 -83
- package/src/hooks/useBuildModeController.tsx +0 -73
- package/src/hooks/useClipboard.tsx +0 -158
- package/src/hooks/useCollapsedGroups.ts +0 -116
- package/src/hooks/useComponentOverride.tsx +0 -59
- package/src/hooks/useCustomizationController.tsx +0 -14
- package/src/hooks/useDialogsController.tsx +0 -15
- package/src/hooks/useEffectiveRoleController.tsx +0 -10
- package/src/hooks/useLargeLayout.tsx +0 -65
- package/src/hooks/useModeController.tsx +0 -23
- package/src/hooks/useNavigationBlocker.tsx +0 -108
- package/src/hooks/usePermissions.ts +0 -45
- package/src/hooks/useRebaseClient.tsx +0 -10
- package/src/hooks/useRebaseContext.tsx +0 -84
- package/src/hooks/useRebaseRegistry.tsx +0 -91
- package/src/hooks/useResolvedComponent.tsx +0 -195
- package/src/hooks/useSlot.tsx +0 -64
- package/src/hooks/useSnackbarController.tsx +0 -80
- package/src/hooks/useStorageSource.tsx +0 -11
- package/src/hooks/useStudioBridge.tsx +0 -258
- package/src/hooks/useTranslation.ts +0 -38
- package/src/hooks/useUnsavedChangesDialog.tsx +0 -67
- package/src/hooks/useUserConfigurationPersistence.tsx +0 -11
- package/src/i18n/RebaseI18nProvider.tsx +0 -159
- package/src/index.ts +0 -32
- package/src/internal/common.tsx +0 -28
- package/src/internal/useRestoreScroll.tsx +0 -69
- package/src/locales/de.ts +0 -941
- package/src/locales/en.ts +0 -1021
- package/src/locales/es.ts +0 -962
- package/src/locales/fr.ts +0 -941
- package/src/locales/hi.ts +0 -941
- package/src/locales/it.ts +0 -941
- package/src/locales/pt.ts +0 -949
- package/src/util/constants.ts +0 -7
- package/src/util/createFormexStub.tsx +0 -66
- package/src/util/entity_cache.ts +0 -242
- package/src/util/enums.ts +0 -34
- package/src/util/icon_list.ts +0 -22
- package/src/util/icons.tsx +0 -129
- package/src/util/index.ts +0 -13
- package/src/util/local_storage.ts +0 -66
- package/src/util/previews.ts +0 -127
- package/src/util/useStorageUploadController.tsx +0 -354
- package/src/vitePlugin.ts +0 -215
- /package/dist/{components/Debug → debug}/UIReferenceView.d.ts +0 -0
- /package/dist/{components/Debug → debug}/UIStyleGuide.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/CardViewDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/CollectionTableDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/KanbanBoardDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/index.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/sample_data.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/CalendarWidget.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/CrmDashboardDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/DashboardMetrics.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/PipelineOverview.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/RecentActivity.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TaskFilters.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TaskTable.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TasksView.d.ts +0 -0
|
@@ -1,571 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { useLocation } from "react-router";
|
|
4
|
-
|
|
5
|
-
import { useData, useRebaseContext } from "../../hooks";
|
|
6
|
-
import { useDataOrder } from "../../hooks/data/useDataOrder";
|
|
7
|
-
import { populateFetchCache } from "../../hooks/data/useFetch";
|
|
8
|
-
import { toFindParams } from "../../hooks/data/collectionQuery";
|
|
9
|
-
import { getRelationIncludeParams } from "../../util/previews";
|
|
10
|
-
import { Entity, EntityReference, EntityRelation, FilterValues, OrderBySpec, OrderByTuple, User, WhereFilterOp, FindResponse } from "@rebasepro/types";
|
|
11
|
-
import { normalizeOrderBy, serializeOrderBy } from "@rebasepro/common";
|
|
12
|
-
import { EntityTableController, RebaseContext, SelectedCellProps, AdminCollection } from "@rebasepro/cms-types";
|
|
13
|
-
import { ScrollRestorationController } from "./useScrollRestoration";
|
|
14
|
-
|
|
15
|
-
export const DEFAULT_PAGE_SIZE = 50;
|
|
16
|
-
|
|
17
|
-
export type DataTableControllerProps<M extends Record<string, any> = any> = {
|
|
18
|
-
/**
|
|
19
|
-
* Full path where the data of this table is located
|
|
20
|
-
*/
|
|
21
|
-
path: string;
|
|
22
|
-
/**
|
|
23
|
-
* The collection that is represented by this config.
|
|
24
|
-
*/
|
|
25
|
-
collection: AdminCollection<M>;
|
|
26
|
-
/**
|
|
27
|
-
* List of entities that will be displayed on top, no matter the ordering.
|
|
28
|
-
* This is used for reference fields selection
|
|
29
|
-
*/
|
|
30
|
-
entitiesDisplayedFirst?: Entity<M>[];
|
|
31
|
-
|
|
32
|
-
lastDeleteTimestamp?: number;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Force filter to be applied to the table.
|
|
36
|
-
*/
|
|
37
|
-
fixedFilter?: FilterValues<string>;
|
|
38
|
-
|
|
39
|
-
scrollRestoration?: ScrollRestorationController;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* When set to true the filters and sort will be updated in the URL
|
|
43
|
-
*/
|
|
44
|
-
updateUrl?: boolean;
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Use this hook to build a controller for the {@link DataCollectionTable}.
|
|
50
|
-
* This controller is bound to data in a path in your specified driver.
|
|
51
|
-
*
|
|
52
|
-
* Note that you can build your own hook returning a {@link EntityTableController}
|
|
53
|
-
* if you would like to display different data.
|
|
54
|
-
*
|
|
55
|
-
* @param path
|
|
56
|
-
* @param collection
|
|
57
|
-
* @param scrollRestoration
|
|
58
|
-
* @param entitiesDisplayedFirst
|
|
59
|
-
* @param lastDeleteTimestamp
|
|
60
|
-
* @param fixedFilterFromProps
|
|
61
|
-
* @param updateUrl
|
|
62
|
-
*/
|
|
63
|
-
export function useDataTableController<M extends Record<string, any> = any, USER extends User = User>(
|
|
64
|
-
{
|
|
65
|
-
path,
|
|
66
|
-
collection,
|
|
67
|
-
scrollRestoration,
|
|
68
|
-
entitiesDisplayedFirst,
|
|
69
|
-
lastDeleteTimestamp: _lastDeleteTimestamp,
|
|
70
|
-
fixedFilter: fixedFilterFromProps,
|
|
71
|
-
updateUrl
|
|
72
|
-
}: DataTableControllerProps<M>)
|
|
73
|
-
: EntityTableController<M> {
|
|
74
|
-
|
|
75
|
-
const {
|
|
76
|
-
defaultFilter,
|
|
77
|
-
sort,
|
|
78
|
-
fixedFilter: fixedFilterFromCollection
|
|
79
|
-
} = collection;
|
|
80
|
-
|
|
81
|
-
const [popupCell, setPopupCell] = React.useState<SelectedCellProps<M> | undefined>(undefined);
|
|
82
|
-
const dataClient = useData();
|
|
83
|
-
|
|
84
|
-
const fixedFilter = fixedFilterFromProps ?? fixedFilterFromCollection;
|
|
85
|
-
const paginationEnabled = collection.pagination === undefined || Boolean(collection.pagination);
|
|
86
|
-
const pageSize = typeof collection.pagination === "number" ? collection.pagination : DEFAULT_PAGE_SIZE;
|
|
87
|
-
|
|
88
|
-
const location = useLocation();
|
|
89
|
-
|
|
90
|
-
const [searchString, setSearchString] = React.useState<string | undefined>(() => {
|
|
91
|
-
if (updateUrl) {
|
|
92
|
-
const params = new URLSearchParams(location.search);
|
|
93
|
-
const urlSearch = params.get("search");
|
|
94
|
-
return urlSearch ? decodeURIComponent(urlSearch) : undefined;
|
|
95
|
-
}
|
|
96
|
-
return undefined;
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const checkFilterCombination = useCallback((filterValues: FilterValues<any>,
|
|
100
|
-
sortBy?: OrderByTuple[]) => {
|
|
101
|
-
// PostgREST/SQL can handle arbitrary filter/sort combinations natively.
|
|
102
|
-
return true;
|
|
103
|
-
}, []);
|
|
104
|
-
|
|
105
|
-
// The collection's default, in the list form the controller works in.
|
|
106
|
-
// `collection.sort` accepts either spelling so a one-key default stays the
|
|
107
|
-
// one-liner it always was.
|
|
108
|
-
const sortInternal = useMemo(() => {
|
|
109
|
-
const keys = normalizeOrderBy(sort as OrderBySpec | undefined);
|
|
110
|
-
if (keys && fixedFilter && !checkFilterCombination(fixedFilter, keys)) {
|
|
111
|
-
console.warn("Initial sort is not compatible with the force filter. Ignoring initial sort");
|
|
112
|
-
return undefined;
|
|
113
|
-
}
|
|
114
|
-
return keys;
|
|
115
|
-
}, [sort, fixedFilter]);
|
|
116
|
-
|
|
117
|
-
const {
|
|
118
|
-
filterValues: filterUrl,
|
|
119
|
-
sortBy: sortUrl
|
|
120
|
-
} = parseFilterAndSort(location.search);
|
|
121
|
-
|
|
122
|
-
const [filterValues, setFilterValues] = React.useState<FilterValues<Extract<keyof M, string> | (string & {})> | undefined>(fixedFilter ?? (updateUrl ? filterUrl : undefined) ?? defaultFilter ?? undefined);
|
|
123
|
-
const [sortBy, setSortBy] = React.useState<OrderByTuple<Extract<keyof M, string> | (string & {})>[] | undefined>((updateUrl ? sortUrl : undefined) ?? sortInternal);
|
|
124
|
-
|
|
125
|
-
// Sync filter/sort state from URL on browser navigation (back/forward).
|
|
126
|
-
//
|
|
127
|
-
// Only ever on an actual *change* of the URL, which is not the same as "not
|
|
128
|
-
// the first run". `useUpdateUrl` writes with `window.history.replaceState`,
|
|
129
|
-
// which react-router does not observe, so the `location` read here keeps
|
|
130
|
-
// reporting the search string this view was mounted with — normally the
|
|
131
|
-
// empty one — no matter what the address bar says. The guard used to be a
|
|
132
|
-
// one-shot ref that skipped the first run and armed itself for every run
|
|
133
|
-
// after; the *second* run then parsed that unchanged, empty search and
|
|
134
|
-
// called `setSortBy(undefined)`, throwing away the collection's `sort`
|
|
135
|
-
// before anything had asked for a different one. Re-running is easy to
|
|
136
|
-
// trigger — this effect depends on `fixedFilter`, a prop most callers pass
|
|
137
|
-
// as a fresh object literal.
|
|
138
|
-
//
|
|
139
|
-
// The visible symptom was a collection that ignored its own default sort: it
|
|
140
|
-
// subscribed correctly, then immediately re-subscribed with no `orderBy` at
|
|
141
|
-
// all, and the second answer replaced the first.
|
|
142
|
-
const lastSyncedSearchRef = React.useRef<string>(location.search);
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
if (!updateUrl) return;
|
|
145
|
-
// Unchanged URL — including the initial mount, where the state
|
|
146
|
-
// initialisers have already applied the URL params and the collection
|
|
147
|
-
// defaults — carries nothing to sync.
|
|
148
|
-
if (lastSyncedSearchRef.current === location.search) return;
|
|
149
|
-
lastSyncedSearchRef.current = location.search;
|
|
150
|
-
|
|
151
|
-
const { filterValues: urlFilterValues, sortBy: urlSortBy } = parseFilterAndSort(location.search);
|
|
152
|
-
if (!fixedFilter) {
|
|
153
|
-
setFilterValues((urlFilterValues ?? defaultFilter) as FilterValues<Extract<keyof M, string> | (string & {})> | undefined);
|
|
154
|
-
}
|
|
155
|
-
if (urlSortBy && fixedFilter && !checkFilterCombination(fixedFilter, urlSortBy)) {
|
|
156
|
-
console.warn("URL sort is not compatible with the force filter.");
|
|
157
|
-
} else {
|
|
158
|
-
setSortBy(urlSortBy as OrderByTuple<Extract<keyof M, string> | (string & {})>[] | undefined);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Sync search string from URL
|
|
162
|
-
const urlParams = new URLSearchParams(location.search);
|
|
163
|
-
const urlSearch = urlParams.get("search");
|
|
164
|
-
setSearchString(urlSearch ? decodeURIComponent(urlSearch) : undefined);
|
|
165
|
-
}, [location.search, updateUrl, fixedFilter, checkFilterCombination]);
|
|
166
|
-
|
|
167
|
-
useUpdateUrl(filterValues, sortBy, searchString, updateUrl);
|
|
168
|
-
|
|
169
|
-
const collectionScroll = scrollRestoration?.getCollectionScroll(path, filterValues);
|
|
170
|
-
const initialItemCount = collectionScroll?.data.length ?? pageSize;
|
|
171
|
-
|
|
172
|
-
useEffect(() => {
|
|
173
|
-
if (scrollRestoration) {
|
|
174
|
-
scrollRestoration.updateCollectionScroll({
|
|
175
|
-
path,
|
|
176
|
-
scrollOffset: collectionScroll?.scrollOffset ?? 0,
|
|
177
|
-
data: rawData,
|
|
178
|
-
filters: filterValues
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}, []);
|
|
182
|
-
|
|
183
|
-
const [itemCount, setItemCount] = React.useState<number | undefined>(paginationEnabled ? initialItemCount : undefined);
|
|
184
|
-
|
|
185
|
-
// The whole sort, as one string, so the effect below re-runs when any key
|
|
186
|
-
// changes — a `sortBy` array is a new reference on every render and would
|
|
187
|
-
// re-subscribe on each one if used as a dependency directly.
|
|
188
|
-
const sortKey = sortBy ? serializeOrderBy(sortBy) : undefined;
|
|
189
|
-
|
|
190
|
-
const context: RebaseContext<USER> = useRebaseContext();
|
|
191
|
-
|
|
192
|
-
const [rawData, setRawData] = useState<Entity<M>[]>(collectionScroll?.data ?? []);
|
|
193
|
-
|
|
194
|
-
const onScroll = useCallback(({
|
|
195
|
-
scrollOffset
|
|
196
|
-
}: {
|
|
197
|
-
scrollOffset: number
|
|
198
|
-
}) => {
|
|
199
|
-
if (scrollRestoration) {
|
|
200
|
-
scrollRestoration.updateCollectionScroll({
|
|
201
|
-
path,
|
|
202
|
-
scrollOffset,
|
|
203
|
-
data: rawData,
|
|
204
|
-
filters: filterValues
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}, [scrollRestoration, path, rawData, filterValues]);
|
|
208
|
-
|
|
209
|
-
const [dataLoading, setDataLoading] = useState<boolean>(false);
|
|
210
|
-
const [dataLoadingError, setDataLoadingError] = useState<Error | undefined>();
|
|
211
|
-
const [noMoreToLoad, setNoMoreToLoad] = useState<boolean>(false);
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Clear the user's filters. `fixedFilter` survives — it is the collection
|
|
215
|
-
* forcing a scope, not something the user chose — but `defaultFilter` does
|
|
216
|
-
* not: a default is where the view *opens*, not a floor it can never go
|
|
217
|
-
* below.
|
|
218
|
-
*
|
|
219
|
-
* It used to reset to `defaultFilter`, which made "clear" unable to clear
|
|
220
|
-
* whenever a collection defined one, and every caller here wanted the
|
|
221
|
-
* opposite. `ClearFilterSortButton` is labelled "clear filter" and left one
|
|
222
|
-
* set. `EditorCollectionActionStart` calls this and then re-applies
|
|
223
|
-
* `collection.defaultFilter` on the next line — redundant under the old
|
|
224
|
-
* behaviour, and the clearest statement of what it expected.
|
|
225
|
-
* `FilterPresetsButton` documents "clearing all filters deactivates all
|
|
226
|
-
* chips", which was false: it derives a chip's active state from the
|
|
227
|
-
* filters, so a preset whose values equalled `defaultFilter` switched
|
|
228
|
-
* itself straight back on. That chip could not be turned off at all — click
|
|
229
|
-
* it, the keys are removed, the filter resets to the identical default, and
|
|
230
|
-
* the chip lights up again.
|
|
231
|
-
*/
|
|
232
|
-
const clearFilter = useCallback(() => setFilterValues(fixedFilter ?? undefined), [fixedFilter]);
|
|
233
|
-
|
|
234
|
-
const updateFilterValues = useCallback((updatedFilter: FilterValues<Extract<keyof M, string> | (string & {})> | undefined) => {
|
|
235
|
-
if (fixedFilter) {
|
|
236
|
-
console.warn("Filter is not compatible with the force filter. Ignoring filter");
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (updatedFilter && Object.keys(updatedFilter).length === 0) {
|
|
240
|
-
setFilterValues(undefined);
|
|
241
|
-
} else {
|
|
242
|
-
setFilterValues(updatedFilter);
|
|
243
|
-
}
|
|
244
|
-
}, [fixedFilter]);
|
|
245
|
-
|
|
246
|
-
useEffect(() => {
|
|
247
|
-
|
|
248
|
-
setDataLoading(true);
|
|
249
|
-
|
|
250
|
-
const onEntitiesUpdate = async (entities: Entity<M>[]) => {
|
|
251
|
-
if (collection.callbacks?.afterRead) {
|
|
252
|
-
try {
|
|
253
|
-
// afterRead operates on flat rows; unwrap the Entity view-model
|
|
254
|
-
// before invoking and re-wrap the processed row after.
|
|
255
|
-
entities = await Promise.all(
|
|
256
|
-
entities.map(async (entity) => {
|
|
257
|
-
const processedRow = await collection.callbacks!.afterRead!({
|
|
258
|
-
collection,
|
|
259
|
-
path,
|
|
260
|
-
row: { id: entity.id, ...entity.values },
|
|
261
|
-
context
|
|
262
|
-
});
|
|
263
|
-
return {
|
|
264
|
-
...entity,
|
|
265
|
-
values: processedRow as M
|
|
266
|
-
};
|
|
267
|
-
}));
|
|
268
|
-
} catch (_e: unknown) {
|
|
269
|
-
console.error(_e);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
setDataLoading(false);
|
|
273
|
-
setDataLoadingError(undefined);
|
|
274
|
-
setRawData(entities.map(e => ({
|
|
275
|
-
...e
|
|
276
|
-
// values: sanitizeData(e.values, resolvedCollection.properties)
|
|
277
|
-
})));
|
|
278
|
-
setNoMoreToLoad(!itemCount || entities.length < itemCount);
|
|
279
|
-
|
|
280
|
-
// Pre-populate the entity fetch cache so that navigating to an
|
|
281
|
-
// entity detail view renders instantly with cached data.
|
|
282
|
-
populateFetchCache(path, entities);
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const onError = (error: Error) => {
|
|
286
|
-
console.error("ERROR", error);
|
|
287
|
-
setDataLoading(false);
|
|
288
|
-
setRawData((prev) => prev && prev.length > 0 ? prev : []);
|
|
289
|
-
setDataLoadingError(error);
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
const accessor = dataClient.collection(path);
|
|
293
|
-
|
|
294
|
-
// filterValues is already FilterValues — pass directly to the accessor
|
|
295
|
-
const whereParams = filterValues && Object.keys(filterValues).length > 0 ? filterValues : undefined;
|
|
296
|
-
const orderByParams = sortBy && sortBy.length > 0
|
|
297
|
-
? sortBy.map(([field, direction]) => [String(field), direction] as OrderByTuple)
|
|
298
|
-
: undefined;
|
|
299
|
-
|
|
300
|
-
let unsubscribe: (() => void) | undefined;
|
|
301
|
-
|
|
302
|
-
// Eagerly include relations to avoid N+1 fetches.
|
|
303
|
-
const includeParams = getRelationIncludeParams(collection);
|
|
304
|
-
|
|
305
|
-
if (accessor.listen) {
|
|
306
|
-
// Assembled in one place — see `toFindParams`. Listing the fields
|
|
307
|
-
// here is what let `searchExplain` reach three of four read paths.
|
|
308
|
-
unsubscribe = accessor.listen(
|
|
309
|
-
toFindParams({ where: whereParams, limit: itemCount, orderBy: orderByParams,
|
|
310
|
-
searchString, include: includeParams }),
|
|
311
|
-
(res) => onEntitiesUpdate(res.data as Entity<M>[]), onError);
|
|
312
|
-
} else {
|
|
313
|
-
accessor.find(
|
|
314
|
-
toFindParams({ where: whereParams, limit: itemCount, orderBy: orderByParams,
|
|
315
|
-
searchString, include: includeParams }))
|
|
316
|
-
.then((res) => onEntitiesUpdate(res.data as Entity<M>[]))
|
|
317
|
-
.catch(onError);
|
|
318
|
-
unsubscribe = () => undefined;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return unsubscribe;
|
|
322
|
-
}, [dataClient, path, itemCount, sortKey, filterValues, searchString]);
|
|
323
|
-
|
|
324
|
-
const orderedData = useDataOrder({
|
|
325
|
-
data: rawData,
|
|
326
|
-
entitiesDisplayedFirst
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// hack to fix Firestore listeners firing with incomplete data
|
|
330
|
-
// const data = useDebouncedData(orderedData, {
|
|
331
|
-
// filterValues,
|
|
332
|
-
// sortBy,
|
|
333
|
-
// searchString,
|
|
334
|
-
// lastDeleteTimestamp
|
|
335
|
-
// });
|
|
336
|
-
const data = orderedData;
|
|
337
|
-
|
|
338
|
-
return useMemo(() => ({
|
|
339
|
-
data,
|
|
340
|
-
dataLoading,
|
|
341
|
-
noMoreToLoad,
|
|
342
|
-
dataLoadingError,
|
|
343
|
-
filterValues,
|
|
344
|
-
setFilterValues: updateFilterValues,
|
|
345
|
-
sortBy,
|
|
346
|
-
setSortBy,
|
|
347
|
-
searchString,
|
|
348
|
-
setSearchString,
|
|
349
|
-
clearFilter,
|
|
350
|
-
itemCount,
|
|
351
|
-
setItemCount,
|
|
352
|
-
initialScroll: collectionScroll?.scrollOffset,
|
|
353
|
-
onScroll,
|
|
354
|
-
paginationEnabled,
|
|
355
|
-
pageSize,
|
|
356
|
-
checkFilterCombination,
|
|
357
|
-
popupCell,
|
|
358
|
-
setPopupCell
|
|
359
|
-
}), [
|
|
360
|
-
data,
|
|
361
|
-
dataLoading,
|
|
362
|
-
noMoreToLoad,
|
|
363
|
-
dataLoadingError,
|
|
364
|
-
filterValues,
|
|
365
|
-
updateFilterValues,
|
|
366
|
-
sortBy,
|
|
367
|
-
searchString,
|
|
368
|
-
clearFilter,
|
|
369
|
-
itemCount,
|
|
370
|
-
collectionScroll?.scrollOffset,
|
|
371
|
-
onScroll,
|
|
372
|
-
paginationEnabled,
|
|
373
|
-
pageSize,
|
|
374
|
-
checkFilterCombination,
|
|
375
|
-
popupCell
|
|
376
|
-
]);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function useUpdateUrl<M extends Record<string, any> = any>(
|
|
380
|
-
filterValues: FilterValues<Extract<keyof M, string>> | undefined,
|
|
381
|
-
sortBy: OrderByTuple<Extract<keyof M, string>>[] | undefined,
|
|
382
|
-
searchString: string | undefined,
|
|
383
|
-
updateUrl: boolean | undefined
|
|
384
|
-
) {
|
|
385
|
-
|
|
386
|
-
useEffect(() => {
|
|
387
|
-
if (updateUrl) {
|
|
388
|
-
// Parse existing URL params to preserve non-filter/sort params like __view
|
|
389
|
-
const existingParams = new URLSearchParams(window.location.search);
|
|
390
|
-
const preservedParams = new URLSearchParams();
|
|
391
|
-
|
|
392
|
-
// Preserve params that are not filter/sort related
|
|
393
|
-
existingParams.forEach((value, key) => {
|
|
394
|
-
if (key.startsWith("__") && key !== "__sort" && key !== "__sort_order") {
|
|
395
|
-
preservedParams.set(key, value);
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
const filterSortState = encodeFilterAndSort(filterValues, sortBy);
|
|
400
|
-
const search = searchString ? `search=${encodeURIComponent(searchString)}` : "";
|
|
401
|
-
|
|
402
|
-
// Combine preserved params with filter/sort state
|
|
403
|
-
const preservedString = preservedParams.toString();
|
|
404
|
-
const parts = [preservedString, filterSortState, search].filter(Boolean);
|
|
405
|
-
const state = parts.join("&");
|
|
406
|
-
|
|
407
|
-
const hash = window.location.hash;
|
|
408
|
-
if (state === "")
|
|
409
|
-
window.history.replaceState({}, "", `${window.location.pathname}${hash}`);
|
|
410
|
-
else
|
|
411
|
-
window.history.replaceState({}, "", `?${state}${hash}`);
|
|
412
|
-
}
|
|
413
|
-
}, [filterValues, sortBy, searchString, updateUrl]);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function encodeFilterAndSort(filterValues?: FilterValues<string>, sortBy?: OrderByTuple[] | undefined) {
|
|
417
|
-
const entries: Record<string, string> = {};
|
|
418
|
-
if (sortBy && sortBy.length > 0) {
|
|
419
|
-
// Comma-separated, positionally paired. A single key encodes exactly as
|
|
420
|
-
// it did before — `__sort=name&__sort_order=asc` — so links already out
|
|
421
|
-
// in the world keep working, and a reload of a two-key sort no longer
|
|
422
|
-
// silently comes back sorted by the first key alone.
|
|
423
|
-
entries["__sort"] = sortBy.map(([field]) => encodeURIComponent(field)).join(",");
|
|
424
|
-
entries["__sort_order"] = sortBy.map(([, direction]) => direction).join(",");
|
|
425
|
-
}
|
|
426
|
-
if (filterValues) {
|
|
427
|
-
Object.entries(filterValues).forEach(([key, value]) => {
|
|
428
|
-
if (value) {
|
|
429
|
-
const conditions: [WhereFilterOp, unknown][] = Array.isArray(value[0])
|
|
430
|
-
? (value as [WhereFilterOp, unknown][])
|
|
431
|
-
: [value as [WhereFilterOp, unknown]];
|
|
432
|
-
|
|
433
|
-
const [op, val] = conditions[0] || [];
|
|
434
|
-
if (op) {
|
|
435
|
-
let encodedValue: unknown = val;
|
|
436
|
-
try {
|
|
437
|
-
if (typeof val === "object") {
|
|
438
|
-
if (val instanceof Date) {
|
|
439
|
-
encodedValue = val.toISOString();
|
|
440
|
-
} else if (Array.isArray(val)) {
|
|
441
|
-
encodedValue = JSON.stringify(val, (k, v) => {
|
|
442
|
-
if (v instanceof EntityRelation) {
|
|
443
|
-
return encodeRelation(v);
|
|
444
|
-
}
|
|
445
|
-
if (v instanceof EntityReference) {
|
|
446
|
-
return encodeReference(v);
|
|
447
|
-
}
|
|
448
|
-
return v;
|
|
449
|
-
});
|
|
450
|
-
} else if (val instanceof EntityRelation) {
|
|
451
|
-
encodedValue = encodeRelation(val);
|
|
452
|
-
} else if (val instanceof EntityReference) {
|
|
453
|
-
encodedValue = encodeReference(val);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
} catch (e) {
|
|
457
|
-
encodedValue = val;
|
|
458
|
-
}
|
|
459
|
-
if (encodedValue !== undefined) {
|
|
460
|
-
entries[encodeURIComponent(`${key}_op`)] = encodeURIComponent(op);
|
|
461
|
-
entries[encodeURIComponent(`${key}_value`)] = encodedValue ? encodeURIComponent(String(encodedValue)) : "null";
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
if (!Object.keys(entries).length) {
|
|
468
|
-
return "";
|
|
469
|
-
}
|
|
470
|
-
return Object.entries(entries).map(([key, value]) => `${key}=${value}`).join("&");
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function parseFilterAndSort<M>(search: string): {
|
|
474
|
-
filterValues: FilterValues<string> | undefined,
|
|
475
|
-
sortBy?: OrderByTuple<Extract<keyof M, string>>[]
|
|
476
|
-
} {
|
|
477
|
-
const entries = new URLSearchParams(search);
|
|
478
|
-
const filterValues: FilterValues<string> = {};
|
|
479
|
-
let sortBy: OrderByTuple[] | undefined = undefined;
|
|
480
|
-
entries.forEach((value, key) => {
|
|
481
|
-
if (key === "__sort") {
|
|
482
|
-
const directions = (entries.get("__sort_order") ?? "").split(",");
|
|
483
|
-
const keys = value
|
|
484
|
-
.split(",")
|
|
485
|
-
.map((field) => decodeURIComponent(field).trim())
|
|
486
|
-
.filter(Boolean)
|
|
487
|
-
// A direction missing from the URL — a hand-written link, or the
|
|
488
|
-
// one-key form of an older bookmark — is ascending, the same
|
|
489
|
-
// reading `?orderBy=name` gets everywhere else.
|
|
490
|
-
.map((field, index): OrderByTuple => [field, directions[index] === "desc" ? "desc" : "asc"]);
|
|
491
|
-
sortBy = keys.length > 0 ? keys : undefined;
|
|
492
|
-
} else if (key.endsWith("_op")) {
|
|
493
|
-
const field = key.replace("_op", "");
|
|
494
|
-
const filterOp = decodeURIComponent(value) as WhereFilterOp;
|
|
495
|
-
const filterValStr = entries.get(`${field}_value`);
|
|
496
|
-
if (filterValStr !== null) {
|
|
497
|
-
filterValues[field] = [filterOp, decodeString(filterValStr)];
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
return {
|
|
503
|
-
filterValues: Object.keys(filterValues).length ? filterValues : undefined,
|
|
504
|
-
sortBy
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function isDate(dateString: string): boolean {
|
|
509
|
-
// Define a regex pattern that matches the exact date format: 2025-01-07T23:00:00.000Z
|
|
510
|
-
const regexPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
511
|
-
|
|
512
|
-
// Test the dateString against the regex pattern
|
|
513
|
-
if (!regexPattern.test(dateString)) {
|
|
514
|
-
return false;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// If the regex matches, further validate if it is a valid UTC date
|
|
518
|
-
const date = new Date(dateString);
|
|
519
|
-
return date.toISOString() === dateString;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function encodeReference(val: EntityReference) {
|
|
523
|
-
return `ref::${val.path}/${val.id}`;
|
|
524
|
-
}
|
|
525
|
-
function encodeRelation(val: EntityRelation) {
|
|
526
|
-
return `rel::${val.path}/${val.id}`;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function decodeString(val: string): EntityReference | EntityRelation | Date | string {
|
|
530
|
-
let parsedFilterVal: EntityReference | EntityRelation | Date | string = val;
|
|
531
|
-
if (isDate(val)) {
|
|
532
|
-
try {
|
|
533
|
-
parsedFilterVal = new Date(val);
|
|
534
|
-
} catch (_e) {
|
|
535
|
-
// ignore
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
if (typeof parsedFilterVal === "string") {
|
|
539
|
-
try {
|
|
540
|
-
parsedFilterVal = JSON.parse(parsedFilterVal, (key, value) => {
|
|
541
|
-
if (typeof value === "string") {
|
|
542
|
-
if (value.startsWith("ref::")) {
|
|
543
|
-
const [path, id] = value.substring(5).split("/");
|
|
544
|
-
return new EntityReference({ id,
|
|
545
|
-
path });
|
|
546
|
-
}
|
|
547
|
-
if (value.startsWith("rel::")) {
|
|
548
|
-
const [path, id] = value.substring(5).split("/");
|
|
549
|
-
return new EntityRelation(id, path);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
return value;
|
|
553
|
-
});
|
|
554
|
-
} catch (_e) {
|
|
555
|
-
// ignore
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
if (typeof parsedFilterVal === "string") {
|
|
560
|
-
if (parsedFilterVal.startsWith("ref::")) {
|
|
561
|
-
const [path, id] = parsedFilterVal.substring(5).split("/");
|
|
562
|
-
return new EntityReference({ id,
|
|
563
|
-
path });
|
|
564
|
-
}
|
|
565
|
-
if (parsedFilterVal.startsWith("rel::")) {
|
|
566
|
-
const [path, id] = parsedFilterVal.substring(5).split("/");
|
|
567
|
-
return new EntityRelation(id, path);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
return parsedFilterVal;
|
|
571
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { deepEqual as equal } from "fast-equals";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Hack to prevent data updates for incomplete callbacks from Firestore
|
|
6
|
-
* triggers.
|
|
7
|
-
* If any deps change, the update is immediate
|
|
8
|
-
* @param data
|
|
9
|
-
* @param deps
|
|
10
|
-
* @param timeoutMs
|
|
11
|
-
*/
|
|
12
|
-
export function useDebouncedData<T>(data: T[], deps: unknown, timeoutMs = 5000) {
|
|
13
|
-
|
|
14
|
-
const [deferredData, setDeferredData] = React.useState(data);
|
|
15
|
-
const dataLength = React.useRef(deferredData.length ?? 0);
|
|
16
|
-
const pendingUpdate = React.useRef(false);
|
|
17
|
-
const currentDeps = React.useRef(deps);
|
|
18
|
-
|
|
19
|
-
const haveDepsChanged = !equal(currentDeps.current, deps);
|
|
20
|
-
const immediateUpdate = data.length >= dataLength.current || haveDepsChanged;
|
|
21
|
-
|
|
22
|
-
React.useEffect(() => {
|
|
23
|
-
|
|
24
|
-
const performUpdate = () => {
|
|
25
|
-
if (!equal(deferredData, data)) {
|
|
26
|
-
currentDeps.current = deps;
|
|
27
|
-
dataLength.current = data.length;
|
|
28
|
-
setDeferredData(data);
|
|
29
|
-
}
|
|
30
|
-
pendingUpdate.current = false;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
pendingUpdate.current = true;
|
|
34
|
-
|
|
35
|
-
let handler: ReturnType<typeof setTimeout> | undefined;
|
|
36
|
-
if (immediateUpdate)
|
|
37
|
-
performUpdate()
|
|
38
|
-
else
|
|
39
|
-
handler = setTimeout(performUpdate, timeoutMs);
|
|
40
|
-
|
|
41
|
-
return () => {
|
|
42
|
-
clearTimeout(handler);
|
|
43
|
-
if (pendingUpdate.current && immediateUpdate)
|
|
44
|
-
performUpdate();
|
|
45
|
-
};
|
|
46
|
-
}, [data, timeoutMs, deps, immediateUpdate]);
|
|
47
|
-
|
|
48
|
-
return immediateUpdate ? data : deferredData;
|
|
49
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { Entity, FilterValues } from "@rebasepro/types";
|
|
2
|
-
|
|
3
|
-
const collectionScrollCache = new Map<string, { scrollOffset: number, data: Entity<any>[] }>();
|
|
4
|
-
|
|
5
|
-
export type ScrollRestorationController = {
|
|
6
|
-
|
|
7
|
-
getCollectionScroll: (path: string,
|
|
8
|
-
filters?: FilterValues<any>) => {
|
|
9
|
-
scrollOffset: number,
|
|
10
|
-
data: Entity<any>[]
|
|
11
|
-
} | undefined;
|
|
12
|
-
|
|
13
|
-
updateCollectionScroll: (props: {
|
|
14
|
-
path: 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
|
-
path,
|
|
26
|
-
filters,
|
|
27
|
-
scrollOffset,
|
|
28
|
-
data
|
|
29
|
-
}: {
|
|
30
|
-
path: string;
|
|
31
|
-
filters?: FilterValues<any>;
|
|
32
|
-
sort?: [string, "asc" | "desc"];
|
|
33
|
-
scrollOffset: number;
|
|
34
|
-
data: Entity<any>[]
|
|
35
|
-
}) => {
|
|
36
|
-
collectionScrollCache.set(
|
|
37
|
-
createCacheKey(path, filters),
|
|
38
|
-
{
|
|
39
|
-
scrollOffset,
|
|
40
|
-
data
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const getCollectionScroll = (path: string,
|
|
45
|
-
filters?: FilterValues<any>) => {
|
|
46
|
-
return collectionScrollCache.get(createCacheKey(path, filters));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
getCollectionScroll,
|
|
51
|
-
updateCollectionScroll
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function createCacheKey(path: string, filters?: FilterValues<any>) {
|
|
56
|
-
|
|
57
|
-
if (!filters) {
|
|
58
|
-
return path;
|
|
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 `${path}?${filtersString}`;
|
|
68
|
-
}
|