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