@rebasepro/app 0.0.1-canary.4829d6e

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