@rebasepro/app 0.0.1-canary.4829d6e
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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/auth/api.d.ts +66 -0
- package/dist/auth/index.d.ts +13 -0
- package/dist/auth/types.d.ts +127 -0
- package/dist/auth/useRebaseAuthController.d.ts +13 -0
- package/dist/components/AIIcon.d.ts +17 -0
- package/dist/components/AuthSimulationMenu.d.ts +0 -0
- package/dist/components/ConfirmationDialog.d.ts +9 -0
- package/dist/components/Debug/UIReferenceView.d.ts +13 -0
- package/dist/components/Debug/UIStyleGuide.d.ts +2 -0
- package/dist/components/Debug/collection-views/CardViewDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/CollectionTableDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/KanbanBoardDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/index.d.ts +3 -0
- package/dist/components/Debug/collection-views/sample_data.d.ts +23 -0
- package/dist/components/Debug/crm-dashboard/CalendarWidget.d.ts +22 -0
- package/dist/components/Debug/crm-dashboard/CrmDashboardDemo.d.ts +2 -0
- package/dist/components/Debug/crm-dashboard/DashboardMetrics.d.ts +15 -0
- package/dist/components/Debug/crm-dashboard/PipelineOverview.d.ts +34 -0
- package/dist/components/Debug/crm-dashboard/RecentActivity.d.ts +19 -0
- package/dist/components/Debug/crm-dashboard/TaskFilters.d.ts +30 -0
- package/dist/components/Debug/crm-dashboard/TaskTable.d.ts +50 -0
- package/dist/components/Debug/crm-dashboard/TasksView.d.ts +41 -0
- package/dist/components/ErrorTooltip.d.ts +3 -0
- package/dist/components/ErrorView.d.ts +21 -0
- package/dist/components/LanguageToggle.d.ts +2 -0
- package/dist/components/LoginView/LoginView.d.ts +113 -0
- package/dist/components/LoginView/index.d.ts +2 -0
- package/dist/components/NotFoundPage.d.ts +2 -0
- package/dist/components/RebaseAuth.d.ts +10 -0
- package/dist/components/RebaseLogo.d.ts +7 -0
- package/dist/components/SchemaDriftBanner.d.ts +27 -0
- package/dist/components/UnsavedChangesDialog.d.ts +9 -0
- package/dist/components/UserDisplay.d.ts +7 -0
- package/dist/components/UserSelectPopover.d.ts +63 -0
- package/dist/components/UserSettingsView.d.ts +2 -0
- package/dist/components/common/index.d.ts +6 -0
- package/dist/components/common/table_height.d.ts +5 -0
- package/dist/components/common/types.d.ts +66 -0
- package/dist/components/common/useColumnsIds.d.ts +9 -0
- package/dist/components/common/useDataTableController.d.ts +45 -0
- package/dist/components/common/useDebouncedData.d.ts +9 -0
- package/dist/components/common/useScrollRestoration.d.ts +14 -0
- package/dist/components/index.d.ts +18 -0
- package/dist/contexts/AdminModeController.d.ts +4 -0
- package/dist/contexts/AnalyticsContext.d.ts +3 -0
- package/dist/contexts/AuthControllerContext.d.ts +3 -0
- package/dist/contexts/CollectionScopeContext.d.ts +50 -0
- package/dist/contexts/ComponentOverrideContext.d.ts +42 -0
- package/dist/contexts/CustomizationControllerContext.d.ts +3 -0
- package/dist/contexts/DataDriverContext.d.ts +3 -0
- package/dist/contexts/DataSourcesContext.d.ts +31 -0
- package/dist/contexts/DatabaseAdminContext.d.ts +3 -0
- package/dist/contexts/DialogsProvider.d.ts +4 -0
- package/dist/contexts/EffectiveRoleController.d.ts +4 -0
- package/dist/contexts/ModeController.d.ts +4 -0
- package/dist/contexts/RebaseClientInstanceContext.d.ts +6 -0
- package/dist/contexts/RebaseDataContext.d.ts +3 -0
- package/dist/contexts/SnackbarProvider.d.ts +2 -0
- package/dist/contexts/StorageSourceContext.d.ts +3 -0
- package/dist/contexts/StorageSourcesContext.d.ts +25 -0
- package/dist/contexts/UserConfigurationPersistenceContext.d.ts +3 -0
- package/dist/contexts/index.d.ts +17 -0
- package/dist/core/PluginLifecycleManager.d.ts +17 -0
- package/dist/core/PluginProviderStack.d.ts +21 -0
- package/dist/core/Rebase.d.ts +15 -0
- package/dist/core/RebaseProps.d.ts +291 -0
- package/dist/core/RebaseRouter.d.ts +4 -0
- package/dist/core/RebaseRoutes.d.ts +17 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/hooks/ApiConfigContext.d.ts +24 -0
- package/dist/hooks/data/delete.d.ts +33 -0
- package/dist/hooks/data/save.d.ts +40 -0
- package/dist/hooks/data/useCollection.d.ts +62 -0
- package/dist/hooks/data/useData.d.ts +13 -0
- package/dist/hooks/data/useDataOrder.d.ts +12 -0
- package/dist/hooks/data/useFetch.d.ts +43 -0
- package/dist/hooks/data/useRelationSelector.d.ts +52 -0
- package/dist/hooks/index.d.ts +36 -0
- package/dist/hooks/useAdminModeController.d.ts +19 -0
- package/dist/hooks/useAnalyticsController.d.ts +5 -0
- package/dist/hooks/useAuthController.d.ts +11 -0
- package/dist/hooks/useAuthSubscription.d.ts +2 -0
- package/dist/hooks/useBackendStorageSource.d.ts +30 -0
- package/dist/hooks/useBridgeRegistration.d.ts +18 -0
- package/dist/hooks/useBrowserTitleAndIcon.d.ts +6 -0
- package/dist/hooks/useBuildAdminModeController.d.ts +6 -0
- package/dist/hooks/useBuildEffectiveRoleController.d.ts +8 -0
- package/dist/hooks/useBuildLocalConfigurationPersistence.d.ts +2 -0
- package/dist/hooks/useBuildModeController.d.ts +6 -0
- package/dist/hooks/useClipboard.d.ts +57 -0
- package/dist/hooks/useCollapsedGroups.d.ts +27 -0
- package/dist/hooks/useComponentOverride.d.ts +32 -0
- package/dist/hooks/useCustomizationController.d.ts +11 -0
- package/dist/hooks/useDialogsController.d.ts +11 -0
- package/dist/hooks/useEffectiveRoleController.d.ts +7 -0
- package/dist/hooks/useLargeLayout.d.ts +1 -0
- package/dist/hooks/useModeController.d.ts +19 -0
- package/dist/hooks/usePermissions.d.ts +12 -0
- package/dist/hooks/useRebaseClient.d.ts +5 -0
- package/dist/hooks/useRebaseContext.d.ts +11 -0
- package/dist/hooks/useRebaseRegistry.d.ts +34 -0
- package/dist/hooks/useResolvedComponent.d.ts +47 -0
- package/dist/hooks/useSlot.d.ts +18 -0
- package/dist/hooks/useSnackbarController.d.ts +20 -0
- package/dist/hooks/useStorageSource.d.ts +6 -0
- package/dist/hooks/useStudioBridge.d.ts +79 -0
- package/dist/hooks/useTranslation.d.ts +17 -0
- package/dist/hooks/useUnsavedChangesDialog.d.ts +12 -0
- package/dist/hooks/useUserConfigurationPersistence.d.ts +8 -0
- package/dist/i18n/RebaseI18nProvider.d.ts +33 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.es.js +16843 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +17021 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/internal/common.d.ts +14 -0
- package/dist/internal/useRestoreScroll.d.ts +15 -0
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/en.d.ts +10 -0
- package/dist/locales/es.d.ts +10 -0
- package/dist/locales/fr.d.ts +2 -0
- package/dist/locales/hi.d.ts +2 -0
- package/dist/locales/it.d.ts +2 -0
- package/dist/locales/pt.d.ts +7 -0
- package/dist/util/constants.d.ts +1 -0
- package/dist/util/createFormexStub.d.ts +2 -0
- package/dist/util/entity_cache.d.ts +22 -0
- package/dist/util/enums.d.ts +5 -0
- package/dist/util/icon_list.d.ts +5 -0
- package/dist/util/icons.d.ts +20 -0
- package/dist/util/index.d.ts +8 -0
- package/dist/util/previews.d.ts +4 -0
- package/dist/util/useStorageUploadController.d.ts +44 -0
- package/dist/vitePlugin.d.ts +60 -0
- package/dist/vitePlugin.js +113 -0
- package/package.json +145 -0
- package/src/auth/api.ts +148 -0
- package/src/auth/index.ts +25 -0
- package/src/auth/types.ts +115 -0
- package/src/auth/useRebaseAuthController.ts +328 -0
- package/src/components/AIIcon.tsx +47 -0
- package/src/components/AuthSimulationMenu.tsx +0 -0
- package/src/components/ConfirmationDialog.tsx +48 -0
- package/src/components/Debug/UIReferenceView.tsx +958 -0
- package/src/components/Debug/UIStyleGuide.tsx +156 -0
- package/src/components/Debug/collection-views/CardViewDemo.tsx +85 -0
- package/src/components/Debug/collection-views/CollectionTableDemo.tsx +107 -0
- package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +90 -0
- package/src/components/Debug/collection-views/index.ts +3 -0
- package/src/components/Debug/collection-views/sample_data.ts +42 -0
- package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +389 -0
- package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +1102 -0
- package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +133 -0
- package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +290 -0
- package/src/components/Debug/crm-dashboard/RecentActivity.tsx +215 -0
- package/src/components/Debug/crm-dashboard/TaskFilters.tsx +184 -0
- package/src/components/Debug/crm-dashboard/TaskTable.tsx +427 -0
- package/src/components/Debug/crm-dashboard/TasksView.tsx +234 -0
- package/src/components/ErrorTooltip.tsx +12 -0
- package/src/components/ErrorView.tsx +94 -0
- package/src/components/LanguageToggle.tsx +59 -0
- package/src/components/LoginView/LoginView.tsx +932 -0
- package/src/components/LoginView/index.ts +2 -0
- package/src/components/NotFoundPage.tsx +27 -0
- package/src/components/RebaseAuth.tsx +27 -0
- package/src/components/RebaseLogo.tsx +29 -0
- package/src/components/SchemaDriftBanner.tsx +102 -0
- package/src/components/UnsavedChangesDialog.tsx +46 -0
- package/src/components/UserDisplay.tsx +54 -0
- package/src/components/UserSelectPopover.tsx +399 -0
- package/src/components/UserSettingsView.tsx +318 -0
- package/src/components/common/index.ts +6 -0
- package/src/components/common/table_height.tsx +21 -0
- package/src/components/common/types.tsx +66 -0
- package/src/components/common/useColumnsIds.tsx +222 -0
- package/src/components/common/useDataTableController.tsx +514 -0
- package/src/components/common/useDebouncedData.ts +49 -0
- package/src/components/common/useScrollRestoration.tsx +68 -0
- package/src/components/index.tsx +26 -0
- package/src/contexts/AdminModeController.tsx +11 -0
- package/src/contexts/AnalyticsContext.tsx +4 -0
- package/src/contexts/AuthControllerContext.tsx +4 -0
- package/src/contexts/CollectionScopeContext.tsx +67 -0
- package/src/contexts/ComponentOverrideContext.tsx +81 -0
- package/src/contexts/CustomizationControllerContext.tsx +4 -0
- package/src/contexts/DataDriverContext.tsx +4 -0
- package/src/contexts/DataSourcesContext.tsx +36 -0
- package/src/contexts/DatabaseAdminContext.tsx +4 -0
- package/src/contexts/DialogsProvider.tsx +56 -0
- package/src/contexts/EffectiveRoleController.tsx +12 -0
- package/src/contexts/ModeController.tsx +11 -0
- package/src/contexts/RebaseClientInstanceContext.tsx +7 -0
- package/src/contexts/RebaseDataContext.tsx +4 -0
- package/src/contexts/SnackbarProvider.tsx +15 -0
- package/src/contexts/StorageSourceContext.tsx +4 -0
- package/src/contexts/StorageSourcesContext.tsx +30 -0
- package/src/contexts/UserConfigurationPersistenceContext.tsx +4 -0
- package/src/contexts/index.ts +17 -0
- package/src/core/PluginLifecycleManager.tsx +95 -0
- package/src/core/PluginProviderStack.tsx +40 -0
- package/src/core/Rebase.tsx +439 -0
- package/src/core/RebaseProps.tsx +320 -0
- package/src/core/RebaseRouter.tsx +17 -0
- package/src/core/RebaseRoutes.tsx +24 -0
- package/src/core/index.tsx +5 -0
- package/src/hooks/ApiConfigContext.tsx +41 -0
- package/src/hooks/data/delete.ts +55 -0
- package/src/hooks/data/save.ts +77 -0
- package/src/hooks/data/useCollection.tsx +176 -0
- package/src/hooks/data/useData.tsx +18 -0
- package/src/hooks/data/useDataOrder.ts +26 -0
- package/src/hooks/data/useFetch.tsx +140 -0
- package/src/hooks/data/useRelationSelector.tsx +234 -0
- package/src/hooks/index.tsx +45 -0
- package/src/hooks/useAdminModeController.tsx +23 -0
- package/src/hooks/useAnalyticsController.tsx +8 -0
- package/src/hooks/useAuthController.tsx +14 -0
- package/src/hooks/useAuthSubscription.ts +87 -0
- package/src/hooks/useBackendStorageSource.ts +288 -0
- package/src/hooks/useBridgeRegistration.tsx +32 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +23 -0
- package/src/hooks/useBuildAdminModeController.tsx +24 -0
- package/src/hooks/useBuildEffectiveRoleController.tsx +31 -0
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +75 -0
- package/src/hooks/useBuildModeController.tsx +71 -0
- package/src/hooks/useClipboard.tsx +158 -0
- package/src/hooks/useCollapsedGroups.ts +116 -0
- package/src/hooks/useComponentOverride.tsx +59 -0
- package/src/hooks/useCustomizationController.tsx +14 -0
- package/src/hooks/useDialogsController.tsx +15 -0
- package/src/hooks/useEffectiveRoleController.tsx +10 -0
- package/src/hooks/useLargeLayout.tsx +65 -0
- package/src/hooks/useModeController.tsx +23 -0
- package/src/hooks/usePermissions.ts +44 -0
- package/src/hooks/useRebaseClient.tsx +10 -0
- package/src/hooks/useRebaseContext.tsx +83 -0
- package/src/hooks/useRebaseRegistry.tsx +91 -0
- package/src/hooks/useResolvedComponent.tsx +159 -0
- package/src/hooks/useSlot.tsx +64 -0
- package/src/hooks/useSnackbarController.tsx +52 -0
- package/src/hooks/useStorageSource.tsx +11 -0
- package/src/hooks/useStudioBridge.tsx +209 -0
- package/src/hooks/useTranslation.ts +33 -0
- package/src/hooks/useUnsavedChangesDialog.tsx +64 -0
- package/src/hooks/useUserConfigurationPersistence.tsx +11 -0
- package/src/i18n/RebaseI18nProvider.tsx +160 -0
- package/src/index.ts +25 -0
- package/src/internal/common.tsx +16 -0
- package/src/internal/useRestoreScroll.tsx +69 -0
- package/src/locales/de.ts +902 -0
- package/src/locales/en.ts +980 -0
- package/src/locales/es.ts +922 -0
- package/src/locales/fr.ts +901 -0
- package/src/locales/hi.ts +901 -0
- package/src/locales/it.ts +901 -0
- package/src/locales/pt.ts +909 -0
- package/src/util/constants.ts +7 -0
- package/src/util/createFormexStub.tsx +66 -0
- package/src/util/entity_cache.ts +239 -0
- package/src/util/enums.ts +32 -0
- package/src/util/icon_list.ts +22 -0
- package/src/util/icons.tsx +149 -0
- package/src/util/index.ts +12 -0
- package/src/util/previews.ts +105 -0
- package/src/util/useStorageUploadController.tsx +350 -0
- package/src/vitePlugin.ts +196 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
2
|
+
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useLocation } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
import { useData, useRebaseContext } from "../../hooks";
|
|
6
|
+
import { useDataOrder } from "../../hooks/data/useDataOrder";
|
|
7
|
+
import { populateFetchCache } from "../../hooks/data/useFetch";
|
|
8
|
+
import { Entity, EntityReference, EntityRelation, EntityTableController, FilterValues, RebaseContext, SelectedCellProps, User, WhereFilterOp, FindResponse } from "@rebasepro/types";
|
|
9
|
+
import { ScrollRestorationController } from "./useScrollRestoration";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_PAGE_SIZE = 50;
|
|
12
|
+
|
|
13
|
+
export type DataTableControllerProps<M extends Record<string, any> = any> = {
|
|
14
|
+
/**
|
|
15
|
+
* Full path where the data of this table is located
|
|
16
|
+
*/
|
|
17
|
+
path: string;
|
|
18
|
+
/**
|
|
19
|
+
* The collection that is represented by this config.
|
|
20
|
+
*/
|
|
21
|
+
collection: CollectionConfig<M>;
|
|
22
|
+
/**
|
|
23
|
+
* List of entities that will be displayed on top, no matter the ordering.
|
|
24
|
+
* This is used for reference fields selection
|
|
25
|
+
*/
|
|
26
|
+
entitiesDisplayedFirst?: Entity<M>[];
|
|
27
|
+
|
|
28
|
+
lastDeleteTimestamp?: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Force filter to be applied to the table.
|
|
32
|
+
*/
|
|
33
|
+
fixedFilter?: FilterValues<string>;
|
|
34
|
+
|
|
35
|
+
scrollRestoration?: ScrollRestorationController;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* When set to true the filters and sort will be updated in the URL
|
|
39
|
+
*/
|
|
40
|
+
updateUrl?: boolean;
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Use this hook to build a controller for the {@link DataCollectionTable}.
|
|
46
|
+
* This controller is bound to data in a path in your specified driver.
|
|
47
|
+
*
|
|
48
|
+
* Note that you can build your own hook returning a {@link EntityTableController}
|
|
49
|
+
* if you would like to display different data.
|
|
50
|
+
*
|
|
51
|
+
* @param path
|
|
52
|
+
* @param collection
|
|
53
|
+
* @param scrollRestoration
|
|
54
|
+
* @param entitiesDisplayedFirst
|
|
55
|
+
* @param lastDeleteTimestamp
|
|
56
|
+
* @param fixedFilterFromProps
|
|
57
|
+
* @param updateUrl
|
|
58
|
+
*/
|
|
59
|
+
export function useDataTableController<M extends Record<string, any> = any, USER extends User = User>(
|
|
60
|
+
{
|
|
61
|
+
path,
|
|
62
|
+
collection,
|
|
63
|
+
scrollRestoration,
|
|
64
|
+
entitiesDisplayedFirst,
|
|
65
|
+
lastDeleteTimestamp: _lastDeleteTimestamp,
|
|
66
|
+
fixedFilter: fixedFilterFromProps,
|
|
67
|
+
updateUrl
|
|
68
|
+
}: DataTableControllerProps<M>)
|
|
69
|
+
: EntityTableController<M> {
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
defaultFilter,
|
|
73
|
+
sort,
|
|
74
|
+
fixedFilter: fixedFilterFromCollection
|
|
75
|
+
} = collection;
|
|
76
|
+
|
|
77
|
+
const [popupCell, setPopupCell] = React.useState<SelectedCellProps<M> | undefined>(undefined);
|
|
78
|
+
const dataClient = useData();
|
|
79
|
+
|
|
80
|
+
const fixedFilter = fixedFilterFromProps ?? fixedFilterFromCollection;
|
|
81
|
+
const paginationEnabled = collection.pagination === undefined || Boolean(collection.pagination);
|
|
82
|
+
const pageSize = typeof collection.pagination === "number" ? collection.pagination : DEFAULT_PAGE_SIZE;
|
|
83
|
+
|
|
84
|
+
const location = useLocation();
|
|
85
|
+
|
|
86
|
+
const [searchString, setSearchString] = React.useState<string | undefined>(() => {
|
|
87
|
+
if (updateUrl) {
|
|
88
|
+
const params = new URLSearchParams(location.search);
|
|
89
|
+
const urlSearch = params.get("search");
|
|
90
|
+
return urlSearch ? decodeURIComponent(urlSearch) : undefined;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const checkFilterCombination = useCallback((filterValues: FilterValues<any>,
|
|
96
|
+
sortBy?: [string, "asc" | "desc"]) => {
|
|
97
|
+
// PostgREST/SQL can handle arbitrary filter/sort combinations natively.
|
|
98
|
+
return true;
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const sortInternal = useMemo(() => {
|
|
102
|
+
if (sort && fixedFilter && !checkFilterCombination(fixedFilter, sort)) {
|
|
103
|
+
console.warn("Initial sort is not compatible with the force filter. Ignoring initial sort");
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return sort;
|
|
107
|
+
}, [sort, fixedFilter]);
|
|
108
|
+
|
|
109
|
+
const {
|
|
110
|
+
filterValues: filterUrl,
|
|
111
|
+
sortBy: sortUrl
|
|
112
|
+
} = parseFilterAndSort(location.search);
|
|
113
|
+
|
|
114
|
+
const [filterValues, setFilterValues] = React.useState<FilterValues<Extract<keyof M, string> | (string & {})> | undefined>(fixedFilter ?? (updateUrl ? filterUrl : undefined) ?? defaultFilter ?? undefined);
|
|
115
|
+
const [sortBy, setSortBy] = React.useState<[Extract<keyof M, string> | (string & {}), "asc" | "desc"] | undefined>((updateUrl ? sortUrl : undefined) ?? sortInternal);
|
|
116
|
+
|
|
117
|
+
// Sync filter/sort state from URL on browser navigation (back/forward).
|
|
118
|
+
// Skip initial mount since useState initializers already handle URL params + collection defaults.
|
|
119
|
+
const initialSearchRef = React.useRef<string | null>(location.search);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!updateUrl) return;
|
|
122
|
+
// Skip the initial mount - useState already set the correct values
|
|
123
|
+
if (initialSearchRef.current !== null && initialSearchRef.current === location.search) {
|
|
124
|
+
initialSearchRef.current = null;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
initialSearchRef.current = null;
|
|
128
|
+
|
|
129
|
+
const { filterValues: urlFilterValues, sortBy: urlSortBy } = parseFilterAndSort(location.search);
|
|
130
|
+
if (!fixedFilter) {
|
|
131
|
+
setFilterValues((urlFilterValues ?? defaultFilter) as FilterValues<Extract<keyof M, string> | (string & {})> | undefined);
|
|
132
|
+
}
|
|
133
|
+
if (urlSortBy && fixedFilter && !checkFilterCombination(fixedFilter, urlSortBy)) {
|
|
134
|
+
console.warn("URL sort is not compatible with the force filter.");
|
|
135
|
+
} else {
|
|
136
|
+
setSortBy(urlSortBy as [Extract<keyof M, string> | (string & {}), "asc" | "desc"] | undefined);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Sync search string from URL
|
|
140
|
+
const urlParams = new URLSearchParams(location.search);
|
|
141
|
+
const urlSearch = urlParams.get("search");
|
|
142
|
+
setSearchString(urlSearch ? decodeURIComponent(urlSearch) : undefined);
|
|
143
|
+
}, [location.search, updateUrl, fixedFilter, checkFilterCombination]);
|
|
144
|
+
|
|
145
|
+
useUpdateUrl(filterValues, sortBy, searchString, updateUrl);
|
|
146
|
+
|
|
147
|
+
const collectionScroll = scrollRestoration?.getCollectionScroll(path, filterValues);
|
|
148
|
+
const initialItemCount = collectionScroll?.data.length ?? pageSize;
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (scrollRestoration) {
|
|
152
|
+
scrollRestoration.updateCollectionScroll({
|
|
153
|
+
path,
|
|
154
|
+
scrollOffset: collectionScroll?.scrollOffset ?? 0,
|
|
155
|
+
data: rawData,
|
|
156
|
+
filters: filterValues
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
const [itemCount, setItemCount] = React.useState<number | undefined>(paginationEnabled ? initialItemCount : undefined);
|
|
162
|
+
|
|
163
|
+
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
164
|
+
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
165
|
+
|
|
166
|
+
const context: RebaseContext<USER> = useRebaseContext();
|
|
167
|
+
|
|
168
|
+
const [rawData, setRawData] = useState<Entity<M>[]>(collectionScroll?.data ?? []);
|
|
169
|
+
|
|
170
|
+
const onScroll = useCallback(({
|
|
171
|
+
scrollOffset
|
|
172
|
+
}: {
|
|
173
|
+
scrollOffset: number
|
|
174
|
+
}) => {
|
|
175
|
+
if (scrollRestoration) {
|
|
176
|
+
scrollRestoration.updateCollectionScroll({
|
|
177
|
+
path,
|
|
178
|
+
scrollOffset,
|
|
179
|
+
data: rawData,
|
|
180
|
+
filters: filterValues
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}, [scrollRestoration, path, rawData, filterValues]);
|
|
184
|
+
|
|
185
|
+
const [dataLoading, setDataLoading] = useState<boolean>(false);
|
|
186
|
+
const [dataLoadingError, setDataLoadingError] = useState<Error | undefined>();
|
|
187
|
+
const [noMoreToLoad, setNoMoreToLoad] = useState<boolean>(false);
|
|
188
|
+
|
|
189
|
+
const clearFilter = useCallback(() => setFilterValues(fixedFilter ?? defaultFilter ?? undefined), [fixedFilter, defaultFilter]);
|
|
190
|
+
|
|
191
|
+
const updateFilterValues = useCallback((updatedFilter: FilterValues<Extract<keyof M, string> | (string & {})> | undefined) => {
|
|
192
|
+
if (fixedFilter) {
|
|
193
|
+
console.warn("Filter is not compatible with the force filter. Ignoring filter");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (updatedFilter && Object.keys(updatedFilter).length === 0) {
|
|
197
|
+
setFilterValues(undefined);
|
|
198
|
+
} else {
|
|
199
|
+
setFilterValues(updatedFilter);
|
|
200
|
+
}
|
|
201
|
+
}, [fixedFilter]);
|
|
202
|
+
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
|
|
205
|
+
setDataLoading(true);
|
|
206
|
+
|
|
207
|
+
const onEntitiesUpdate = async (entities: Entity<M>[]) => {
|
|
208
|
+
if (collection.callbacks?.afterRead) {
|
|
209
|
+
try {
|
|
210
|
+
// afterRead operates on flat rows; unwrap the Entity view-model
|
|
211
|
+
// before invoking and re-wrap the processed row after.
|
|
212
|
+
entities = await Promise.all(
|
|
213
|
+
entities.map(async (entity) => {
|
|
214
|
+
const processedRow = await collection.callbacks!.afterRead!({
|
|
215
|
+
collection,
|
|
216
|
+
path,
|
|
217
|
+
row: { id: entity.id, ...entity.values },
|
|
218
|
+
context
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
...entity,
|
|
222
|
+
values: processedRow as M
|
|
223
|
+
};
|
|
224
|
+
}));
|
|
225
|
+
} catch (_e: unknown) {
|
|
226
|
+
console.error(_e);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
setDataLoading(false);
|
|
230
|
+
setDataLoadingError(undefined);
|
|
231
|
+
setRawData(entities.map(e => ({
|
|
232
|
+
...e
|
|
233
|
+
// values: sanitizeData(e.values, resolvedCollection.properties)
|
|
234
|
+
})));
|
|
235
|
+
setNoMoreToLoad(!itemCount || entities.length < itemCount);
|
|
236
|
+
|
|
237
|
+
// Pre-populate the entity fetch cache so that navigating to an
|
|
238
|
+
// entity detail view renders instantly with cached data.
|
|
239
|
+
populateFetchCache(path, entities);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const onError = (error: Error) => {
|
|
243
|
+
console.error("ERROR", error);
|
|
244
|
+
setDataLoading(false);
|
|
245
|
+
setRawData((prev) => prev && prev.length > 0 ? prev : []);
|
|
246
|
+
setDataLoadingError(error);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const accessor = dataClient.collection(path);
|
|
250
|
+
|
|
251
|
+
// filterValues is already FilterValues — pass directly to the accessor
|
|
252
|
+
const whereParams = filterValues && Object.keys(filterValues).length > 0 ? filterValues : undefined;
|
|
253
|
+
const orderByParams: [string, "asc" | "desc"] | undefined = sortBy ? [String(sortBy[0]), sortBy[1]] : undefined;
|
|
254
|
+
|
|
255
|
+
let unsubscribe: (() => void) | undefined;
|
|
256
|
+
|
|
257
|
+
if (accessor.listen) {
|
|
258
|
+
unsubscribe = accessor.listen({
|
|
259
|
+
where: whereParams,
|
|
260
|
+
limit: itemCount,
|
|
261
|
+
orderBy: orderByParams,
|
|
262
|
+
searchString
|
|
263
|
+
}, (res) => onEntitiesUpdate(res.data as Entity<M>[]), onError);
|
|
264
|
+
} else {
|
|
265
|
+
accessor.find({
|
|
266
|
+
where: whereParams,
|
|
267
|
+
limit: itemCount,
|
|
268
|
+
orderBy: orderByParams,
|
|
269
|
+
searchString
|
|
270
|
+
})
|
|
271
|
+
.then((res) => onEntitiesUpdate(res.data as Entity<M>[]))
|
|
272
|
+
.catch(onError);
|
|
273
|
+
unsubscribe = () => undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return unsubscribe;
|
|
277
|
+
}, [dataClient, path, itemCount, currentSort, sortByProperty, filterValues, searchString]);
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
const orderedData = useDataOrder({
|
|
281
|
+
data: rawData,
|
|
282
|
+
entitiesDisplayedFirst
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// hack to fix Firestore listeners firing with incomplete data
|
|
286
|
+
// const data = useDebouncedData(orderedData, {
|
|
287
|
+
// filterValues,
|
|
288
|
+
// sortBy,
|
|
289
|
+
// searchString,
|
|
290
|
+
// lastDeleteTimestamp
|
|
291
|
+
// });
|
|
292
|
+
const data = orderedData;
|
|
293
|
+
|
|
294
|
+
return useMemo(() => ({
|
|
295
|
+
data,
|
|
296
|
+
dataLoading,
|
|
297
|
+
noMoreToLoad,
|
|
298
|
+
dataLoadingError,
|
|
299
|
+
filterValues,
|
|
300
|
+
setFilterValues: updateFilterValues,
|
|
301
|
+
sortBy,
|
|
302
|
+
setSortBy,
|
|
303
|
+
searchString,
|
|
304
|
+
setSearchString,
|
|
305
|
+
clearFilter,
|
|
306
|
+
itemCount,
|
|
307
|
+
setItemCount,
|
|
308
|
+
initialScroll: collectionScroll?.scrollOffset,
|
|
309
|
+
onScroll,
|
|
310
|
+
paginationEnabled,
|
|
311
|
+
pageSize,
|
|
312
|
+
checkFilterCombination,
|
|
313
|
+
popupCell,
|
|
314
|
+
setPopupCell
|
|
315
|
+
}), [
|
|
316
|
+
data,
|
|
317
|
+
dataLoading,
|
|
318
|
+
noMoreToLoad,
|
|
319
|
+
dataLoadingError,
|
|
320
|
+
filterValues,
|
|
321
|
+
updateFilterValues,
|
|
322
|
+
sortBy,
|
|
323
|
+
searchString,
|
|
324
|
+
clearFilter,
|
|
325
|
+
itemCount,
|
|
326
|
+
collectionScroll?.scrollOffset,
|
|
327
|
+
onScroll,
|
|
328
|
+
paginationEnabled,
|
|
329
|
+
pageSize,
|
|
330
|
+
checkFilterCombination,
|
|
331
|
+
popupCell
|
|
332
|
+
]);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function useUpdateUrl<M extends Record<string, any> = any>(
|
|
336
|
+
filterValues: FilterValues<Extract<keyof M, string>> | undefined,
|
|
337
|
+
sortBy: [Extract<keyof M, string>, "asc" | "desc"] | undefined,
|
|
338
|
+
searchString: string | undefined,
|
|
339
|
+
updateUrl: boolean | undefined
|
|
340
|
+
) {
|
|
341
|
+
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
if (updateUrl) {
|
|
344
|
+
// Parse existing URL params to preserve non-filter/sort params like __view
|
|
345
|
+
const existingParams = new URLSearchParams(window.location.search);
|
|
346
|
+
const preservedParams = new URLSearchParams();
|
|
347
|
+
|
|
348
|
+
// Preserve params that are not filter/sort related
|
|
349
|
+
existingParams.forEach((value, key) => {
|
|
350
|
+
if (key.startsWith("__") && key !== "__sort" && key !== "__sort_order") {
|
|
351
|
+
preservedParams.set(key, value);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const filterSortState = encodeFilterAndSort(filterValues, sortBy);
|
|
356
|
+
const search = searchString ? `search=${encodeURIComponent(searchString)}` : "";
|
|
357
|
+
|
|
358
|
+
// Combine preserved params with filter/sort state
|
|
359
|
+
const preservedString = preservedParams.toString();
|
|
360
|
+
const parts = [preservedString, filterSortState, search].filter(Boolean);
|
|
361
|
+
const state = parts.join("&");
|
|
362
|
+
|
|
363
|
+
const hash = window.location.hash;
|
|
364
|
+
if (state === "")
|
|
365
|
+
window.history.replaceState({}, "", `${window.location.pathname}${hash}`);
|
|
366
|
+
else
|
|
367
|
+
window.history.replaceState({}, "", `?${state}${hash}`);
|
|
368
|
+
}
|
|
369
|
+
}, [filterValues, sortBy, searchString, updateUrl]);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function encodeFilterAndSort(filterValues?: FilterValues<string>, sortBy?: [string, "asc" | "desc"] | undefined) {
|
|
373
|
+
const entries: Record<string, string> = {};
|
|
374
|
+
if (sortBy) {
|
|
375
|
+
entries["__sort"] = encodeURIComponent(sortBy[0]);
|
|
376
|
+
entries["__sort_order"] = encodeURIComponent(sortBy[1]);
|
|
377
|
+
}
|
|
378
|
+
if (filterValues) {
|
|
379
|
+
Object.entries(filterValues).forEach(([key, value]) => {
|
|
380
|
+
if (value) {
|
|
381
|
+
const conditions: [WhereFilterOp, unknown][] = Array.isArray(value[0])
|
|
382
|
+
? (value as [WhereFilterOp, unknown][])
|
|
383
|
+
: [value as [WhereFilterOp, unknown]];
|
|
384
|
+
|
|
385
|
+
const [op, val] = conditions[0] || [];
|
|
386
|
+
if (op) {
|
|
387
|
+
let encodedValue: unknown = val;
|
|
388
|
+
try {
|
|
389
|
+
if (typeof val === "object") {
|
|
390
|
+
if (val instanceof Date) {
|
|
391
|
+
encodedValue = val.toISOString();
|
|
392
|
+
} else if (Array.isArray(val)) {
|
|
393
|
+
encodedValue = JSON.stringify(val, (k, v) => {
|
|
394
|
+
if (v instanceof EntityRelation) {
|
|
395
|
+
return encodeRelation(v);
|
|
396
|
+
}
|
|
397
|
+
if (v instanceof EntityReference) {
|
|
398
|
+
return encodeReference(v);
|
|
399
|
+
}
|
|
400
|
+
return v;
|
|
401
|
+
});
|
|
402
|
+
} else if (val instanceof EntityRelation) {
|
|
403
|
+
encodedValue = encodeRelation(val);
|
|
404
|
+
} else if (val instanceof EntityReference) {
|
|
405
|
+
encodedValue = encodeReference(val);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} catch (e) {
|
|
409
|
+
encodedValue = val;
|
|
410
|
+
}
|
|
411
|
+
if (encodedValue !== undefined) {
|
|
412
|
+
entries[encodeURIComponent(`${key}_op`)] = encodeURIComponent(op);
|
|
413
|
+
entries[encodeURIComponent(`${key}_value`)] = encodedValue ? encodeURIComponent(String(encodedValue)) : "null";
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if (!Object.keys(entries).length) {
|
|
420
|
+
return "";
|
|
421
|
+
}
|
|
422
|
+
return Object.entries(entries).map(([key, value]) => `${key}=${value}`).join("&");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function parseFilterAndSort<M>(search: string): {
|
|
426
|
+
filterValues: FilterValues<string> | undefined,
|
|
427
|
+
sortBy?: [Extract<keyof M, string>, "asc" | "desc"]
|
|
428
|
+
} {
|
|
429
|
+
const entries = new URLSearchParams(search);
|
|
430
|
+
const filterValues: FilterValues<string> = {};
|
|
431
|
+
let sortBy: [string, "asc" | "desc"] | undefined = undefined;
|
|
432
|
+
entries.forEach((value, key) => {
|
|
433
|
+
if (key === "__sort") {
|
|
434
|
+
sortBy = [decodeURIComponent(value), entries.get("__sort_order") as "asc" | "desc"];
|
|
435
|
+
} else if (key.endsWith("_op")) {
|
|
436
|
+
const field = key.replace("_op", "");
|
|
437
|
+
const filterOp = decodeURIComponent(value) as WhereFilterOp;
|
|
438
|
+
const filterValStr = entries.get(`${field}_value`);
|
|
439
|
+
if (filterValStr !== null) {
|
|
440
|
+
filterValues[field] = [filterOp, decodeString(filterValStr)];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
filterValues: Object.keys(filterValues).length ? filterValues : undefined,
|
|
447
|
+
sortBy
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function isDate(dateString: string): boolean {
|
|
452
|
+
// Define a regex pattern that matches the exact date format: 2025-01-07T23:00:00.000Z
|
|
453
|
+
const regexPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
454
|
+
|
|
455
|
+
// Test the dateString against the regex pattern
|
|
456
|
+
if (!regexPattern.test(dateString)) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// If the regex matches, further validate if it is a valid UTC date
|
|
461
|
+
const date = new Date(dateString);
|
|
462
|
+
return date.toISOString() === dateString;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function encodeReference(val: EntityReference) {
|
|
466
|
+
return `ref::${val.path}/${val.id}`;
|
|
467
|
+
}
|
|
468
|
+
function encodeRelation(val: EntityRelation) {
|
|
469
|
+
return `rel::${val.path}/${val.id}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function decodeString(val: string): EntityReference | EntityRelation | Date | string {
|
|
473
|
+
let parsedFilterVal: EntityReference | EntityRelation | Date | string = val;
|
|
474
|
+
if (isDate(val)) {
|
|
475
|
+
try {
|
|
476
|
+
parsedFilterVal = new Date(val);
|
|
477
|
+
} catch (_e) {
|
|
478
|
+
// ignore
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (typeof parsedFilterVal === "string") {
|
|
482
|
+
try {
|
|
483
|
+
parsedFilterVal = JSON.parse(parsedFilterVal, (key, value) => {
|
|
484
|
+
if (typeof value === "string") {
|
|
485
|
+
if (value.startsWith("ref::")) {
|
|
486
|
+
const [path, id] = value.substring(5).split("/");
|
|
487
|
+
return new EntityReference({ id,
|
|
488
|
+
path });
|
|
489
|
+
}
|
|
490
|
+
if (value.startsWith("rel::")) {
|
|
491
|
+
const [path, id] = value.substring(5).split("/");
|
|
492
|
+
return new EntityRelation(id, path);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return value;
|
|
496
|
+
});
|
|
497
|
+
} catch (_e) {
|
|
498
|
+
// ignore
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (typeof parsedFilterVal === "string") {
|
|
503
|
+
if (parsedFilterVal.startsWith("ref::")) {
|
|
504
|
+
const [path, id] = parsedFilterVal.substring(5).split("/");
|
|
505
|
+
return new EntityReference({ id,
|
|
506
|
+
path });
|
|
507
|
+
}
|
|
508
|
+
if (parsedFilterVal.startsWith("rel::")) {
|
|
509
|
+
const [path, id] = parsedFilterVal.substring(5).split("/");
|
|
510
|
+
return new EntityRelation(id, path);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return parsedFilterVal;
|
|
514
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type { ErrorViewProps } from "./ErrorView";
|
|
2
|
+
export { ErrorView } from "./ErrorView";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export * from "./common";
|
|
6
|
+
|
|
7
|
+
export * from "./NotFoundPage";
|
|
8
|
+
|
|
9
|
+
export * from "./ConfirmationDialog";
|
|
10
|
+
export * from "./ErrorTooltip";
|
|
11
|
+
export * from "./RebaseLogo";
|
|
12
|
+
|
|
13
|
+
export * from "./AIIcon";
|
|
14
|
+
export * from "./Debug/UIStyleGuide";
|
|
15
|
+
export * from "./Debug/UIReferenceView";
|
|
16
|
+
export { CrmDashboardDemo } from "./Debug/crm-dashboard/CrmDashboardDemo";
|
|
17
|
+
export * from "./UserSettingsView";
|
|
18
|
+
export * from "./LanguageToggle";
|
|
19
|
+
export * from "./UserSelectPopover";
|
|
20
|
+
export * from "./UserDisplay";
|
|
21
|
+
|
|
22
|
+
export * from "./LoginView";
|
|
23
|
+
|
|
24
|
+
export * from "./RebaseAuth";
|
|
25
|
+
|
|
26
|
+
export * from "./SchemaDriftBanner";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AdminModeController } from "../hooks";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_ADMIN_MODE_STATE: AdminModeController = {
|
|
5
|
+
mode: "content",
|
|
6
|
+
setMode: (mode: "content" | "studio" | "settings") => {
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
export const AdminModeControllerContext = React.createContext<AdminModeController>(DEFAULT_ADMIN_MODE_STATE);
|
|
10
|
+
|
|
11
|
+
export const AdminModeControllerProvider = AdminModeControllerContext.Provider;
|