@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,183 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { useEffect, useState, useMemo } from "react";
|
|
3
|
-
import { Entity, FilterValues, User } from "@rebasepro/types";
|
|
4
|
-
import { useData } from "./useData";
|
|
5
|
-
import { isSchemaDriftError, useSchemaDriftContext } from "../../components/SchemaDriftBanner";
|
|
6
|
-
import { toFindParams } from "./collectionQuery";
|
|
7
|
-
import { getRelationIncludeParams } from "../../util/previews";
|
|
8
|
-
import type { AdminCollection } from "@rebasepro/cms-types";
|
|
9
|
-
/**
|
|
10
|
-
* @group Hooks and utilities
|
|
11
|
-
*/
|
|
12
|
-
export interface CollectionProps<M extends Record<string, any>> {
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Absolute collection path
|
|
16
|
-
*/
|
|
17
|
-
path: string;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* collection of the entity displayed by this collection
|
|
21
|
-
*/
|
|
22
|
-
collection: AdminCollection<M>
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Number of entities to fetch
|
|
26
|
-
*/
|
|
27
|
-
itemCount?: number;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Number of items to skip
|
|
31
|
-
*/
|
|
32
|
-
offset?: number;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Page number (1-indexed), alternative to offset
|
|
36
|
-
*/
|
|
37
|
-
page?: number;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Filter the fetched data by the property
|
|
41
|
-
*/
|
|
42
|
-
filterValues?: FilterValues<Extract<keyof M, string>>;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Sort the results by
|
|
46
|
-
*/
|
|
47
|
-
sortBy?: [Extract<keyof M, string>, "asc" | "desc"];
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Search string
|
|
51
|
-
*/
|
|
52
|
-
searchString?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* @group Hooks and utilities
|
|
57
|
-
*/
|
|
58
|
-
export interface CollectionResult<M extends Record<string, any>> {
|
|
59
|
-
data: Entity<M>[];
|
|
60
|
-
dataLoading: boolean;
|
|
61
|
-
noMoreToLoad: boolean;
|
|
62
|
-
dataLoadingError?: Error;
|
|
63
|
-
totalCount?: number;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* This hook is used to fetch collections using a given collection
|
|
68
|
-
* @param path
|
|
69
|
-
* @param collection
|
|
70
|
-
* @param filterValues
|
|
71
|
-
* @param sortBy
|
|
72
|
-
* @param itemCount
|
|
73
|
-
* @param offset
|
|
74
|
-
* @param page
|
|
75
|
-
* @param searchString
|
|
76
|
-
* @group Hooks and utilities
|
|
77
|
-
*/
|
|
78
|
-
export function useCollection<M extends Record<string, any>, USER extends User>(
|
|
79
|
-
{
|
|
80
|
-
path,
|
|
81
|
-
collection,
|
|
82
|
-
filterValues,
|
|
83
|
-
sortBy,
|
|
84
|
-
itemCount,
|
|
85
|
-
offset,
|
|
86
|
-
page,
|
|
87
|
-
searchString
|
|
88
|
-
}: CollectionProps<M>): CollectionResult<M> {
|
|
89
|
-
const dataClient = useData();
|
|
90
|
-
const { reportSchemaDrift } = useSchemaDriftContext();
|
|
91
|
-
|
|
92
|
-
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
93
|
-
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
94
|
-
// Map to PostgREST format for orderBy
|
|
95
|
-
const orderByParams: [string, "asc" | "desc"] | undefined = sortBy ? [String(sortBy[0]), sortBy[1]] : undefined;
|
|
96
|
-
|
|
97
|
-
// filterValues is already FilterValues — pass directly
|
|
98
|
-
const whereParams = filterValues && Object.keys(filterValues).length > 0 ? filterValues : undefined;
|
|
99
|
-
|
|
100
|
-
const [data, setData] = useState<Entity<M>[]>([]);
|
|
101
|
-
|
|
102
|
-
const [dataLoading, setDataLoading] = useState<boolean>(false);
|
|
103
|
-
const [dataLoadingError, setDataLoadingError] = useState<Error | undefined>();
|
|
104
|
-
const [noMoreToLoad, setNoMoreToLoad] = useState<boolean>(false);
|
|
105
|
-
const [totalCount, setTotalCount] = useState<number | undefined>();
|
|
106
|
-
|
|
107
|
-
useEffect(() => {
|
|
108
|
-
|
|
109
|
-
setDataLoading(true);
|
|
110
|
-
|
|
111
|
-
const onEntitiesUpdate = async (res: { data: Entity<M>[], meta: { hasMore: boolean; total?: number } }) => {
|
|
112
|
-
const entities = res.data;
|
|
113
|
-
setDataLoading(false);
|
|
114
|
-
setDataLoadingError(undefined);
|
|
115
|
-
setData(entities.map(e => ({
|
|
116
|
-
...e
|
|
117
|
-
})));
|
|
118
|
-
setNoMoreToLoad(!res.meta.hasMore);
|
|
119
|
-
setTotalCount(res.meta.total);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const onError = (error: Error) => {
|
|
123
|
-
console.error("ERROR", error);
|
|
124
|
-
setDataLoading(false);
|
|
125
|
-
setData([]);
|
|
126
|
-
setDataLoadingError(error);
|
|
127
|
-
setTotalCount(undefined);
|
|
128
|
-
// Report schema drift to the global banner context
|
|
129
|
-
if (isSchemaDriftError(error)) {
|
|
130
|
-
reportSchemaDrift(error.message);
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const accessor = dataClient.collection(path);
|
|
135
|
-
|
|
136
|
-
// Eagerly include relations to avoid N+1 fetches.
|
|
137
|
-
const includeParams = getRelationIncludeParams(collection);
|
|
138
|
-
|
|
139
|
-
if (accessor.listen) {
|
|
140
|
-
// Assembled in one place — see `toFindParams`.
|
|
141
|
-
return accessor.listen(
|
|
142
|
-
toFindParams({ where: whereParams, limit: itemCount, offset, page,
|
|
143
|
-
orderBy: orderByParams, searchString, include: includeParams }),
|
|
144
|
-
(res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
|
|
145
|
-
meta: res.meta }), onError);
|
|
146
|
-
} else {
|
|
147
|
-
// The one-shot fallback, taken whenever the client has no socket.
|
|
148
|
-
// `listen` above cancels on cleanup, and every dependency of this
|
|
149
|
-
// effect — the search string, the filters, the sort, the page —
|
|
150
|
-
// changes while a request is in flight. A promise cannot be
|
|
151
|
-
// unsubscribed, so the cleanup has to disown its result instead:
|
|
152
|
-
// without this, whichever response arrives last wins, and responses
|
|
153
|
-
// do not arrive in request order. Typing into search would settle
|
|
154
|
-
// on the results for whichever query the server happened to finish
|
|
155
|
-
// last.
|
|
156
|
-
let cancelled = false;
|
|
157
|
-
accessor.find(
|
|
158
|
-
toFindParams({ where: whereParams, limit: itemCount, offset, page,
|
|
159
|
-
orderBy: orderByParams, searchString, include: includeParams }))
|
|
160
|
-
.then((res) => {
|
|
161
|
-
if (cancelled) return;
|
|
162
|
-
onEntitiesUpdate({ data: res.data as Entity<M>[],
|
|
163
|
-
meta: res.meta });
|
|
164
|
-
})
|
|
165
|
-
.catch((e) => {
|
|
166
|
-
if (cancelled) return;
|
|
167
|
-
onError(e);
|
|
168
|
-
});
|
|
169
|
-
return () => {
|
|
170
|
-
cancelled = true;
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
}, [path, itemCount, offset, page, currentSort, sortByProperty, filterValues, searchString, dataClient, collection]);
|
|
174
|
-
|
|
175
|
-
return useMemo(() => ({
|
|
176
|
-
data,
|
|
177
|
-
dataLoading,
|
|
178
|
-
dataLoadingError,
|
|
179
|
-
noMoreToLoad,
|
|
180
|
-
totalCount
|
|
181
|
-
}), [data, dataLoading, dataLoadingError, noMoreToLoad, totalCount]);
|
|
182
|
-
|
|
183
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { useContext } from "react";
|
|
2
|
-
import { RebaseData } from "@rebasepro/types";
|
|
3
|
-
import { RebaseDataContext } from "../../contexts/RebaseDataContext";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Use this hook to access the unified data API.
|
|
7
|
-
*
|
|
8
|
-
* ```ts
|
|
9
|
-
* const data = useData();
|
|
10
|
-
* const { data: products } = await data.products.find({ where: { status: ["==", "published"] } });
|
|
11
|
-
* await data.products.create({ name: "Camera", price: 299 });
|
|
12
|
-
* ```
|
|
13
|
-
*
|
|
14
|
-
* @group Hooks and utilities
|
|
15
|
-
*/
|
|
16
|
-
export const useData = (): RebaseData => {
|
|
17
|
-
return useContext(RebaseDataContext);
|
|
18
|
-
};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { Entity } from "@rebasepro/types";
|
|
2
|
-
|
|
3
|
-
export interface DataOrderProps<M extends Record<string, any>> {
|
|
4
|
-
data: Entity<M>[];
|
|
5
|
-
entitiesDisplayedFirst?: Entity<M>[];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* This hook is used to have some entities at the beginning of data.
|
|
10
|
-
* @param path
|
|
11
|
-
* @param entitiesDisplayedFirst
|
|
12
|
-
* @group Hooks and utilities
|
|
13
|
-
*/
|
|
14
|
-
export function useDataOrder<M extends Record<string, any>>(
|
|
15
|
-
{
|
|
16
|
-
data,
|
|
17
|
-
entitiesDisplayedFirst
|
|
18
|
-
}: DataOrderProps<M>): Entity<M>[] {
|
|
19
|
-
|
|
20
|
-
if (!entitiesDisplayedFirst)
|
|
21
|
-
return data;
|
|
22
|
-
|
|
23
|
-
const displayedFirstId = new Set(entitiesDisplayedFirst.map((e) => e.id));
|
|
24
|
-
return [...entitiesDisplayedFirst, ...data.filter((e) => !displayedFirstId.has(e.id))];
|
|
25
|
-
|
|
26
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { useEffect, useState, useMemo } from "react";
|
|
3
|
-
import { Entity, User } from "@rebasepro/types";
|
|
4
|
-
import { RebaseContext, AdminCollection } from "@rebasepro/cms-types";
|
|
5
|
-
import { useData } from "./useData";
|
|
6
|
-
import { useRebaseContext } from "../useRebaseContext";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @group Hooks and utilities
|
|
10
|
-
*/
|
|
11
|
-
export interface FetchProps<M extends Record<string, any>, USER extends User = User> {
|
|
12
|
-
path: string;
|
|
13
|
-
entityId?: string | number;
|
|
14
|
-
databaseId?: string;
|
|
15
|
-
collection: AdminCollection<M, USER>;
|
|
16
|
-
useCache?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @group Hooks and utilities
|
|
21
|
-
*/
|
|
22
|
-
export interface FetchResult<M extends Record<string, any>> {
|
|
23
|
-
entity?: Entity<M>,
|
|
24
|
-
dataLoading: boolean,
|
|
25
|
-
dataLoadingError?: Error
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const CACHE: Record<string, Entity<any> | undefined> = {};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Pre-populate the entity fetch cache with entities loaded from a collection.
|
|
32
|
-
* This allows entity detail views to render instantly using cached data,
|
|
33
|
-
* while the background fetch/listener brings in fresh data.
|
|
34
|
-
* @param path - The collection path (e.g. "products")
|
|
35
|
-
* @param entities - Array of entities to cache
|
|
36
|
-
*/
|
|
37
|
-
export function populateFetchCache<M extends Record<string, any>>(path: string, entities: Entity<M>[]): void {
|
|
38
|
-
for (const entity of entities) {
|
|
39
|
-
CACHE[`${path}/${entity.id}`] = entity;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Clear the entity fetch cache. Call this on auth state changes (e.g. logout)
|
|
45
|
-
* to prevent stale data from a previous session leaking into the next.
|
|
46
|
-
*/
|
|
47
|
-
export function clearFetchCache(): void {
|
|
48
|
-
for (const key of Object.keys(CACHE)) {
|
|
49
|
-
delete CACHE[key];
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* This hook is used to fetch a entity.
|
|
55
|
-
* It gives real time updates if the driver supports it.
|
|
56
|
-
* @param path
|
|
57
|
-
* @param collection
|
|
58
|
-
* @param entityId
|
|
59
|
-
* @param useCache
|
|
60
|
-
* @group Hooks and utilities
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
|
-
export function useFetch<M extends Record<string, any>, USER extends User = User>(
|
|
64
|
-
{
|
|
65
|
-
path,
|
|
66
|
-
entityId,
|
|
67
|
-
collection,
|
|
68
|
-
databaseId,
|
|
69
|
-
useCache = false
|
|
70
|
-
}: FetchProps<M, USER>): FetchResult<M> {
|
|
71
|
-
|
|
72
|
-
const dataClient = useData();
|
|
73
|
-
|
|
74
|
-
const context: RebaseContext<USER> = useRebaseContext();
|
|
75
|
-
|
|
76
|
-
// Seed initial state from the cache to avoid skeleton flashes.
|
|
77
|
-
// Even when useCache is false, we show cached data instantly while
|
|
78
|
-
// the background fetch/listener brings in fresh data.
|
|
79
|
-
const cacheKey = entityId ? `${path}/${entityId}` : undefined;
|
|
80
|
-
const cachedEntity = cacheKey ? CACHE[cacheKey] as Entity<M> | undefined : undefined;
|
|
81
|
-
|
|
82
|
-
const [entity, setEntity] = useState<Entity<M> | undefined>(cachedEntity);
|
|
83
|
-
const [dataLoading, setDataLoading] = useState<boolean>(!cachedEntity);
|
|
84
|
-
const [dataLoadingError, setDataLoadingError] = useState<Error | undefined>();
|
|
85
|
-
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
|
|
88
|
-
// Only show loading state if we have no cached entity to display
|
|
89
|
-
if (!CACHE[`${path}/${entityId}`]) {
|
|
90
|
-
setDataLoading(true);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const onEntityUpdate = async (updatedEntity?: Entity<M> | null) => {
|
|
94
|
-
CACHE[`${path}/${entityId}`] = updatedEntity ?? undefined;
|
|
95
|
-
setEntity(updatedEntity ?? undefined);
|
|
96
|
-
setDataLoading(false);
|
|
97
|
-
setDataLoadingError(undefined);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const onError = (error: Error) => {
|
|
101
|
-
console.error("ERROR fetching entity", error);
|
|
102
|
-
setDataLoading(false);
|
|
103
|
-
setEntity(undefined);
|
|
104
|
-
setDataLoadingError(error);
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
if (entityId && useCache && CACHE[`${path}/${entityId}`]) {
|
|
108
|
-
setEntity(CACHE[`${path}/${entityId}`]);
|
|
109
|
-
setDataLoading(false);
|
|
110
|
-
setDataLoadingError(undefined);
|
|
111
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
112
|
-
return () => {
|
|
113
|
-
};
|
|
114
|
-
} else if (entityId && path && collection) {
|
|
115
|
-
const accessor = dataClient.collection(path);
|
|
116
|
-
|
|
117
|
-
if (accessor.listenById) {
|
|
118
|
-
return accessor.listenById(entityId, (entity) => onEntityUpdate(entity as Entity<M> | undefined), onError);
|
|
119
|
-
} else {
|
|
120
|
-
// The one-shot fallback, taken whenever the client has no
|
|
121
|
-
// socket. `listenById` above cancels on cleanup; a promise
|
|
122
|
-
// cannot, so the cleanup disowns its result instead. `entityId`
|
|
123
|
-
// is an effect dependency, and where this hook's caller is not
|
|
124
|
-
// remounted per record — the side panel replaces its contents
|
|
125
|
-
// in place — a slow response for the record just left would
|
|
126
|
-
// otherwise land on top of the one now showing.
|
|
127
|
-
let cancelled = false;
|
|
128
|
-
accessor.findById(entityId)
|
|
129
|
-
.then((entity) => {
|
|
130
|
-
if (cancelled) return;
|
|
131
|
-
onEntityUpdate(entity as Entity<M> | undefined);
|
|
132
|
-
})
|
|
133
|
-
.catch((e) => {
|
|
134
|
-
if (cancelled) return;
|
|
135
|
-
onError(e);
|
|
136
|
-
});
|
|
137
|
-
return () => {
|
|
138
|
-
cancelled = true;
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// if no entityId is provided we do nothing
|
|
143
|
-
else {
|
|
144
|
-
onEntityUpdate(undefined);
|
|
145
|
-
return () => {
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
}, [entityId, path, dataClient, collection, useCache, databaseId]);
|
|
149
|
-
|
|
150
|
-
return useMemo(() => ({
|
|
151
|
-
entity,
|
|
152
|
-
dataLoading,
|
|
153
|
-
dataLoadingError
|
|
154
|
-
}), [entity, dataLoading, dataLoadingError]);
|
|
155
|
-
|
|
156
|
-
}
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
|
3
|
-
import { useData } from "./useData";
|
|
4
|
-
import { Entity, EntityRelation, FilterValues } from "@rebasepro/types";
|
|
5
|
-
import { getRelationIncludeParams } from "../../util/previews";
|
|
6
|
-
import type { AdminCollection } from "@rebasepro/cms-types";
|
|
7
|
-
export interface RelationItem {
|
|
8
|
-
id: string | number;
|
|
9
|
-
label: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
data: Entity<any>;
|
|
12
|
-
relation: EntityRelation;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface UseRelationSelectorProps<M extends Record<string, any> = any> {
|
|
16
|
-
/**
|
|
17
|
-
* Full path where the relation data is located
|
|
18
|
-
*/
|
|
19
|
-
path: string;
|
|
20
|
-
/**
|
|
21
|
-
* The collection that represents the relation entities
|
|
22
|
-
*/
|
|
23
|
-
collection: AdminCollection<M>;
|
|
24
|
-
/**
|
|
25
|
-
* Force filter to be applied to the relation search
|
|
26
|
-
*/
|
|
27
|
-
fixedFilter?: FilterValues<string>;
|
|
28
|
-
/**
|
|
29
|
-
* Page size for pagination
|
|
30
|
-
*/
|
|
31
|
-
pageSize?: number;
|
|
32
|
-
/**
|
|
33
|
-
* Function to extract the label from a entity
|
|
34
|
-
*/
|
|
35
|
-
getLabelFromEntity?: (entity: Entity<M>) => string;
|
|
36
|
-
/**
|
|
37
|
-
* Function to extract the description from a entity
|
|
38
|
-
*/
|
|
39
|
-
getDescriptionFromEntity?: (entity: Entity<M>) => string | undefined;
|
|
40
|
-
/**
|
|
41
|
-
* Property name to use as the secondary display field
|
|
42
|
-
*/
|
|
43
|
-
descriptionProperty?: keyof M;
|
|
44
|
-
/**
|
|
45
|
-
* Whether the list should be fetched at all. Defaults to `true`.
|
|
46
|
-
*
|
|
47
|
-
* A picker that is mounted is not a picker that is open, and the two used
|
|
48
|
-
* to be the same thing here: the fetch ran on mount, so a collection table
|
|
49
|
-
* with a relation column paid for one query — or one realtime subscription
|
|
50
|
-
* — per rendered row before anyone clicked a cell. Pass `false` until the
|
|
51
|
-
* list is actually needed and nothing is requested; flipping it to `true`
|
|
52
|
-
* fetches once, and it never goes back.
|
|
53
|
-
*/
|
|
54
|
-
enabled?: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface RelationSelectorController {
|
|
58
|
-
items: RelationItem[];
|
|
59
|
-
isLoading: boolean;
|
|
60
|
-
error: Error | undefined;
|
|
61
|
-
search: (searchString: string) => void;
|
|
62
|
-
loadMore: () => void;
|
|
63
|
-
hasMore: boolean;
|
|
64
|
-
entityToRelationItem: (entity: Entity<any>, relation: EntityRelation) => RelationItem;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const DEFAULT_PAGE_SIZE = 10;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Hook to manage relation selection with data fetching from Rebase data source
|
|
71
|
-
*/
|
|
72
|
-
export function useRelationSelector<M extends Record<string, any> = any>(
|
|
73
|
-
{
|
|
74
|
-
path,
|
|
75
|
-
collection,
|
|
76
|
-
fixedFilter,
|
|
77
|
-
pageSize = DEFAULT_PAGE_SIZE,
|
|
78
|
-
getLabelFromEntity,
|
|
79
|
-
getDescriptionFromEntity,
|
|
80
|
-
descriptionProperty,
|
|
81
|
-
enabled = true
|
|
82
|
-
}: UseRelationSelectorProps<M>
|
|
83
|
-
): RelationSelectorController {
|
|
84
|
-
|
|
85
|
-
const dataClient = useData();
|
|
86
|
-
|
|
87
|
-
const [items, setItems] = useState<RelationItem[]>([]);
|
|
88
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
89
|
-
const isLoadingRef = useRef(false);
|
|
90
|
-
const [error, setError] = useState<Error | undefined>();
|
|
91
|
-
const [hasMore, setHasMore] = useState(true);
|
|
92
|
-
const [currentSearch, setCurrentSearch] = useState<string>("");
|
|
93
|
-
const [limit, setLimit] = useState<number>(pageSize);
|
|
94
|
-
|
|
95
|
-
const unsubscribeRef = useRef<(() => void) | null>(null);
|
|
96
|
-
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
97
|
-
// Whether a fetch has ever completed. Between `enabled` flipping and the
|
|
98
|
-
// effect running there is one paint with no items and no request in
|
|
99
|
-
// flight; without this the list shows "no results" for that frame.
|
|
100
|
-
const hasLoadedRef = useRef(false);
|
|
101
|
-
|
|
102
|
-
const setLoading = useCallback((loading: boolean) => {
|
|
103
|
-
isLoadingRef.current = loading;
|
|
104
|
-
setIsLoading(loading);
|
|
105
|
-
}, []);
|
|
106
|
-
|
|
107
|
-
// Function to convert entity to RelationItem
|
|
108
|
-
const entityToRelationItem = useCallback((entity: Entity<M>, relation?: EntityRelation): RelationItem => {
|
|
109
|
-
let label: string;
|
|
110
|
-
let description: string | undefined;
|
|
111
|
-
|
|
112
|
-
if (getLabelFromEntity) {
|
|
113
|
-
label = getLabelFromEntity(entity);
|
|
114
|
-
} else {
|
|
115
|
-
// Fallback: try common label properties
|
|
116
|
-
const commonLabelProps = ["name", "title", "label", "displayName"];
|
|
117
|
-
let foundProp: string | undefined;
|
|
118
|
-
|
|
119
|
-
if (entity.values) {
|
|
120
|
-
foundProp = commonLabelProps.find(prop => entity.values[prop] != null && entity.values[prop] !== "");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (foundProp && entity.values[foundProp]) {
|
|
124
|
-
label = String(entity.values[foundProp]);
|
|
125
|
-
} else {
|
|
126
|
-
// Ultimate fallback: use entity ID
|
|
127
|
-
label = String(entity.id);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (getDescriptionFromEntity) {
|
|
132
|
-
description = getDescriptionFromEntity(entity);
|
|
133
|
-
} else if (descriptionProperty && entity.values && entity.values[descriptionProperty]) {
|
|
134
|
-
description = String(entity.values[descriptionProperty]);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
id: entity.id,
|
|
139
|
-
label,
|
|
140
|
-
description,
|
|
141
|
-
data: entity,
|
|
142
|
-
relation: relation ? relation : new EntityRelation(entity.id, path)
|
|
143
|
-
};
|
|
144
|
-
}, [getLabelFromEntity, getDescriptionFromEntity, descriptionProperty]);
|
|
145
|
-
|
|
146
|
-
// Clean up any existing subscription
|
|
147
|
-
const cleanupSubscription = useCallback(() => {
|
|
148
|
-
if (unsubscribeRef.current) {
|
|
149
|
-
unsubscribeRef.current();
|
|
150
|
-
unsubscribeRef.current = null;
|
|
151
|
-
}
|
|
152
|
-
}, []);
|
|
153
|
-
|
|
154
|
-
// Eagerly include relations to avoid N+1 fetches. Referentially stable, so
|
|
155
|
-
// it can be a dependency below without re-triggering the fetch.
|
|
156
|
-
const includeParams = getRelationIncludeParams(collection);
|
|
157
|
-
|
|
158
|
-
const fetchData = useCallback(() => {
|
|
159
|
-
cleanupSubscription();
|
|
160
|
-
setError(undefined);
|
|
161
|
-
setLoading(true);
|
|
162
|
-
|
|
163
|
-
// fixedFilter is already FilterValues — pass directly
|
|
164
|
-
const whereParams = fixedFilter && Object.keys(fixedFilter).length > 0 ? fixedFilter : undefined;
|
|
165
|
-
|
|
166
|
-
const onEntitiesUpdate = (res: { data: Entity<M>[], meta: { hasMore: boolean } }) => {
|
|
167
|
-
const newItems = res.data.map((e) => entityToRelationItem(e));
|
|
168
|
-
hasLoadedRef.current = true;
|
|
169
|
-
setItems(newItems);
|
|
170
|
-
setHasMore(res.meta.hasMore);
|
|
171
|
-
setLoading(false);
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const onErrorUpdate = (fetchError: Error) => {
|
|
175
|
-
console.error("useRelationSelector: Error fetching data:", fetchError);
|
|
176
|
-
hasLoadedRef.current = true;
|
|
177
|
-
setError(fetchError);
|
|
178
|
-
setLoading(false);
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const accessor = dataClient.collection(path);
|
|
182
|
-
|
|
183
|
-
let unsubscribe: (() => void) | undefined;
|
|
184
|
-
|
|
185
|
-
if (accessor.listen) {
|
|
186
|
-
unsubscribe = accessor.listen({
|
|
187
|
-
where: whereParams,
|
|
188
|
-
limit: limit,
|
|
189
|
-
orderBy: undefined,
|
|
190
|
-
searchString: currentSearch,
|
|
191
|
-
include: includeParams
|
|
192
|
-
}, (res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
|
|
193
|
-
meta: res.meta }), onErrorUpdate);
|
|
194
|
-
} else {
|
|
195
|
-
// The one-shot fallback, taken whenever the client has no socket.
|
|
196
|
-
// `cleanupSubscription` is what keeps this hook's results matching
|
|
197
|
-
// the query that asked for them, and it can only do that if the
|
|
198
|
-
// fallback hands back something that actually cancels. A promise
|
|
199
|
-
// cannot be unsubscribed, so this disowns its result instead —
|
|
200
|
-
// otherwise the debounce is the only thing between a slow response
|
|
201
|
-
// for "ab" and its landing on top of the results for "abc".
|
|
202
|
-
let cancelled = false;
|
|
203
|
-
accessor.find({
|
|
204
|
-
where: whereParams,
|
|
205
|
-
limit: limit,
|
|
206
|
-
offset: 0,
|
|
207
|
-
orderBy: undefined,
|
|
208
|
-
searchString: currentSearch,
|
|
209
|
-
include: includeParams
|
|
210
|
-
})
|
|
211
|
-
.then((res) => {
|
|
212
|
-
if (cancelled) return;
|
|
213
|
-
onEntitiesUpdate({ data: res.data as Entity<M>[],
|
|
214
|
-
meta: res.meta });
|
|
215
|
-
})
|
|
216
|
-
.catch((e) => {
|
|
217
|
-
if (cancelled) return;
|
|
218
|
-
onErrorUpdate(e);
|
|
219
|
-
});
|
|
220
|
-
unsubscribe = () => {
|
|
221
|
-
cancelled = true;
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
unsubscribeRef.current = unsubscribe || null;
|
|
226
|
-
}, [dataClient, path, fixedFilter, limit, currentSearch, entityToRelationItem, cleanupSubscription, setLoading, includeParams]);
|
|
227
|
-
|
|
228
|
-
// Search function with debouncing
|
|
229
|
-
const search = useCallback((searchString: string) => {
|
|
230
|
-
// Clear existing timeout
|
|
231
|
-
if (searchTimeoutRef.current) {
|
|
232
|
-
clearTimeout(searchTimeoutRef.current);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Debounce search
|
|
236
|
-
searchTimeoutRef.current = setTimeout(() => {
|
|
237
|
-
setLimit(pageSize);
|
|
238
|
-
setCurrentSearch(searchString);
|
|
239
|
-
}, searchString.trim() ? 300 : 0);
|
|
240
|
-
}, [pageSize]);
|
|
241
|
-
|
|
242
|
-
// Load more function
|
|
243
|
-
const loadMore = useCallback(() => {
|
|
244
|
-
if (!isLoadingRef.current && hasMore && items.length > 0) {
|
|
245
|
-
setLoading(true);
|
|
246
|
-
setLimit(prev => prev + pageSize);
|
|
247
|
-
}
|
|
248
|
-
}, [hasMore, items.length, pageSize, setLoading]);
|
|
249
|
-
|
|
250
|
-
// Load initial data and update upon changes
|
|
251
|
-
useEffect(() => {
|
|
252
|
-
if (!enabled) return;
|
|
253
|
-
fetchData();
|
|
254
|
-
|
|
255
|
-
return () => {
|
|
256
|
-
cleanupSubscription();
|
|
257
|
-
};
|
|
258
|
-
}, [fetchData, enabled, cleanupSubscription]);
|
|
259
|
-
|
|
260
|
-
useEffect(() => {
|
|
261
|
-
return () => {
|
|
262
|
-
if (searchTimeoutRef.current) {
|
|
263
|
-
clearTimeout(searchTimeoutRef.current);
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
}, []);
|
|
267
|
-
|
|
268
|
-
return useMemo(() => ({
|
|
269
|
-
items,
|
|
270
|
-
// Enabled but not answered yet still counts as loading, so the frame
|
|
271
|
-
// between opening the picker and the request starting shows a spinner
|
|
272
|
-
// rather than "no results".
|
|
273
|
-
isLoading: isLoading || (enabled && !hasLoadedRef.current),
|
|
274
|
-
error,
|
|
275
|
-
search,
|
|
276
|
-
loadMore,
|
|
277
|
-
hasMore,
|
|
278
|
-
entityToRelationItem
|
|
279
|
-
}), [items, isLoading, error, search, loadMore, hasMore, entityToRelationItem, enabled]);
|
|
280
|
-
}
|