@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.
Files changed (185) hide show
  1. package/README.md +4 -1
  2. package/dist/components/index.d.ts +0 -3
  3. package/dist/contexts/AuthControllerContext.d.ts +2 -1
  4. package/dist/contexts/CustomizationControllerContext.d.ts +2 -1
  5. package/dist/contexts/DialogsProvider.d.ts +2 -1
  6. package/dist/contexts/RebaseDataContext.d.ts +10 -1
  7. package/dist/contexts/StorageSourceContext.d.ts +11 -1
  8. package/dist/core/RebaseProps.d.ts +2 -12
  9. package/dist/debug/index.d.ts +19 -0
  10. package/dist/debug.js +3631 -0
  11. package/dist/hooks/data/delete.d.ts +16 -7
  12. package/dist/hooks/data/save.d.ts +18 -4
  13. package/dist/hooks/index.d.ts +1 -0
  14. package/dist/hooks/useAdminModeController.d.ts +5 -4
  15. package/dist/hooks/useNavigationGroupLabel.d.ts +17 -0
  16. package/dist/hooks/useStudioBridge.d.ts +33 -0
  17. package/dist/index.es.js +2599 -6980
  18. package/dist/index.es.js.map +1 -1
  19. package/dist/vitePlugin.d.ts +22 -0
  20. package/dist/vitePlugin.js +40 -1
  21. package/package.json +42 -31
  22. package/src/auth/api.ts +0 -134
  23. package/src/auth/index.ts +0 -26
  24. package/src/auth/types.ts +0 -115
  25. package/src/auth/useRebaseAuthController.ts +0 -345
  26. package/src/collections/collection_view_config.ts +0 -76
  27. package/src/collections/entity-display-cache.ts +0 -195
  28. package/src/collections/entity-display.ts +0 -77
  29. package/src/collections/entity_image_preview.ts +0 -61
  30. package/src/collections/filter-operator-resolution.ts +0 -119
  31. package/src/collections/form-layout.ts +0 -394
  32. package/src/collections/index.ts +0 -13
  33. package/src/collections/navigation_from_path.ts +0 -123
  34. package/src/collections/navigation_utils.ts +0 -171
  35. package/src/collections/parent_references_from_path.ts +0 -59
  36. package/src/collections/property-path.ts +0 -30
  37. package/src/collections/property_presentation.ts +0 -264
  38. package/src/collections/summary-property.ts +0 -114
  39. package/src/collections/title-property.ts +0 -312
  40. package/src/components/AIIcon.tsx +0 -47
  41. package/src/components/AuthSimulationMenu.tsx +0 -0
  42. package/src/components/ConfirmationDialog.tsx +0 -48
  43. package/src/components/Debug/UIReferenceView.tsx +0 -1053
  44. package/src/components/Debug/UIStyleGuide.tsx +0 -159
  45. package/src/components/Debug/collection-views/CardViewDemo.tsx +0 -87
  46. package/src/components/Debug/collection-views/CollectionTableDemo.tsx +0 -107
  47. package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +0 -89
  48. package/src/components/Debug/collection-views/index.ts +0 -3
  49. package/src/components/Debug/collection-views/sample_data.ts +0 -44
  50. package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +0 -387
  51. package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +0 -1100
  52. package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +0 -131
  53. package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +0 -288
  54. package/src/components/Debug/crm-dashboard/RecentActivity.tsx +0 -213
  55. package/src/components/Debug/crm-dashboard/TaskFilters.tsx +0 -182
  56. package/src/components/Debug/crm-dashboard/TaskTable.tsx +0 -425
  57. package/src/components/Debug/crm-dashboard/TasksView.tsx +0 -234
  58. package/src/components/ErrorTooltip.tsx +0 -12
  59. package/src/components/ErrorView.tsx +0 -94
  60. package/src/components/LanguageToggle.tsx +0 -59
  61. package/src/components/LoginView/LoginView.tsx +0 -1235
  62. package/src/components/LoginView/index.ts +0 -2
  63. package/src/components/LoginView/oauth-redirect-flow.ts +0 -184
  64. package/src/components/NotFoundPage.tsx +0 -27
  65. package/src/components/RebaseAuth.tsx +0 -27
  66. package/src/components/RebaseLogo.tsx +0 -88
  67. package/src/components/SchemaDriftBanner.tsx +0 -102
  68. package/src/components/UnsavedChangesDialog.tsx +0 -46
  69. package/src/components/UserDisplay.tsx +0 -54
  70. package/src/components/UserSelectPopover.tsx +0 -399
  71. package/src/components/UserSettingsView.tsx +0 -318
  72. package/src/components/common/index.ts +0 -6
  73. package/src/components/common/table_height.tsx +0 -21
  74. package/src/components/common/types.tsx +0 -66
  75. package/src/components/common/useColumnsIds.tsx +0 -261
  76. package/src/components/common/useDataTableController.tsx +0 -571
  77. package/src/components/common/useDebouncedData.ts +0 -49
  78. package/src/components/common/useScrollRestoration.tsx +0 -68
  79. package/src/components/index.tsx +0 -26
  80. package/src/contexts/AdminModeController.tsx +0 -11
  81. package/src/contexts/AnalyticsContext.tsx +0 -4
  82. package/src/contexts/AuthControllerContext.tsx +0 -4
  83. package/src/contexts/CollectionResolverContext.tsx +0 -24
  84. package/src/contexts/CollectionScopeContext.tsx +0 -68
  85. package/src/contexts/ComponentOverrideContext.tsx +0 -81
  86. package/src/contexts/CustomizationControllerContext.tsx +0 -4
  87. package/src/contexts/DataDriverContext.tsx +0 -4
  88. package/src/contexts/DataSourcesContext.tsx +0 -36
  89. package/src/contexts/DatabaseAdminContext.tsx +0 -4
  90. package/src/contexts/DialogsProvider.tsx +0 -56
  91. package/src/contexts/EffectiveRoleController.tsx +0 -12
  92. package/src/contexts/ModeController.tsx +0 -11
  93. package/src/contexts/RebaseClientInstanceContext.tsx +0 -7
  94. package/src/contexts/RebaseDataContext.tsx +0 -4
  95. package/src/contexts/SnackbarProvider.tsx +0 -15
  96. package/src/contexts/StorageSourceContext.tsx +0 -4
  97. package/src/contexts/StorageSourcesContext.tsx +0 -30
  98. package/src/contexts/UserConfigurationPersistenceContext.tsx +0 -4
  99. package/src/contexts/index.ts +0 -18
  100. package/src/core/PluginLifecycleManager.tsx +0 -96
  101. package/src/core/PluginProviderStack.tsx +0 -40
  102. package/src/core/Rebase.tsx +0 -488
  103. package/src/core/RebaseProps.tsx +0 -333
  104. package/src/core/RebaseRouter.tsx +0 -18
  105. package/src/core/RebaseRoutes.tsx +0 -24
  106. package/src/core/index.tsx +0 -5
  107. package/src/hooks/ApiConfigContext.tsx +0 -83
  108. package/src/hooks/data/collectionQuery.ts +0 -55
  109. package/src/hooks/data/delete.ts +0 -56
  110. package/src/hooks/data/save.ts +0 -78
  111. package/src/hooks/data/useCollection.tsx +0 -183
  112. package/src/hooks/data/useData.tsx +0 -18
  113. package/src/hooks/data/useDataOrder.ts +0 -26
  114. package/src/hooks/data/useFetch.tsx +0 -156
  115. package/src/hooks/data/useRelationSelector.tsx +0 -280
  116. package/src/hooks/index.tsx +0 -45
  117. package/src/hooks/useAdminModeController.tsx +0 -23
  118. package/src/hooks/useAnalyticsController.tsx +0 -8
  119. package/src/hooks/useAuthController.tsx +0 -15
  120. package/src/hooks/useAuthSubscription.ts +0 -130
  121. package/src/hooks/useBackendStorageSource.ts +0 -295
  122. package/src/hooks/useBridgeRegistration.tsx +0 -32
  123. package/src/hooks/useBrowserTitleAndIcon.tsx +0 -23
  124. package/src/hooks/useBuildAdminModeController.tsx +0 -29
  125. package/src/hooks/useBuildEffectiveRoleController.tsx +0 -30
  126. package/src/hooks/useBuildLocalConfigurationPersistence.tsx +0 -83
  127. package/src/hooks/useBuildModeController.tsx +0 -73
  128. package/src/hooks/useClipboard.tsx +0 -158
  129. package/src/hooks/useCollapsedGroups.ts +0 -116
  130. package/src/hooks/useComponentOverride.tsx +0 -59
  131. package/src/hooks/useCustomizationController.tsx +0 -14
  132. package/src/hooks/useDialogsController.tsx +0 -15
  133. package/src/hooks/useEffectiveRoleController.tsx +0 -10
  134. package/src/hooks/useLargeLayout.tsx +0 -65
  135. package/src/hooks/useModeController.tsx +0 -23
  136. package/src/hooks/useNavigationBlocker.tsx +0 -108
  137. package/src/hooks/usePermissions.ts +0 -45
  138. package/src/hooks/useRebaseClient.tsx +0 -10
  139. package/src/hooks/useRebaseContext.tsx +0 -84
  140. package/src/hooks/useRebaseRegistry.tsx +0 -91
  141. package/src/hooks/useResolvedComponent.tsx +0 -195
  142. package/src/hooks/useSlot.tsx +0 -64
  143. package/src/hooks/useSnackbarController.tsx +0 -80
  144. package/src/hooks/useStorageSource.tsx +0 -11
  145. package/src/hooks/useStudioBridge.tsx +0 -258
  146. package/src/hooks/useTranslation.ts +0 -38
  147. package/src/hooks/useUnsavedChangesDialog.tsx +0 -67
  148. package/src/hooks/useUserConfigurationPersistence.tsx +0 -11
  149. package/src/i18n/RebaseI18nProvider.tsx +0 -159
  150. package/src/index.ts +0 -32
  151. package/src/internal/common.tsx +0 -28
  152. package/src/internal/useRestoreScroll.tsx +0 -69
  153. package/src/locales/de.ts +0 -941
  154. package/src/locales/en.ts +0 -1021
  155. package/src/locales/es.ts +0 -962
  156. package/src/locales/fr.ts +0 -941
  157. package/src/locales/hi.ts +0 -941
  158. package/src/locales/it.ts +0 -941
  159. package/src/locales/pt.ts +0 -949
  160. package/src/util/constants.ts +0 -7
  161. package/src/util/createFormexStub.tsx +0 -66
  162. package/src/util/entity_cache.ts +0 -242
  163. package/src/util/enums.ts +0 -34
  164. package/src/util/icon_list.ts +0 -22
  165. package/src/util/icons.tsx +0 -129
  166. package/src/util/index.ts +0 -13
  167. package/src/util/local_storage.ts +0 -66
  168. package/src/util/previews.ts +0 -127
  169. package/src/util/useStorageUploadController.tsx +0 -354
  170. package/src/vitePlugin.ts +0 -215
  171. /package/dist/{components/Debug → debug}/UIReferenceView.d.ts +0 -0
  172. /package/dist/{components/Debug → debug}/UIStyleGuide.d.ts +0 -0
  173. /package/dist/{components/Debug → debug}/collection-views/CardViewDemo.d.ts +0 -0
  174. /package/dist/{components/Debug → debug}/collection-views/CollectionTableDemo.d.ts +0 -0
  175. /package/dist/{components/Debug → debug}/collection-views/KanbanBoardDemo.d.ts +0 -0
  176. /package/dist/{components/Debug → debug}/collection-views/index.d.ts +0 -0
  177. /package/dist/{components/Debug → debug}/collection-views/sample_data.d.ts +0 -0
  178. /package/dist/{components/Debug → debug}/crm-dashboard/CalendarWidget.d.ts +0 -0
  179. /package/dist/{components/Debug → debug}/crm-dashboard/CrmDashboardDemo.d.ts +0 -0
  180. /package/dist/{components/Debug → debug}/crm-dashboard/DashboardMetrics.d.ts +0 -0
  181. /package/dist/{components/Debug → debug}/crm-dashboard/PipelineOverview.d.ts +0 -0
  182. /package/dist/{components/Debug → debug}/crm-dashboard/RecentActivity.d.ts +0 -0
  183. /package/dist/{components/Debug → debug}/crm-dashboard/TaskFilters.d.ts +0 -0
  184. /package/dist/{components/Debug → debug}/crm-dashboard/TaskTable.d.ts +0 -0
  185. /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
- }