@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,350 @@
1
+
2
+ import type { ArrayProperty, Property, StringProperty } from "@rebasepro/types";
3
+ import Compressor from "compressorjs";
4
+ import { deepEqual as equal } from "fast-equals";
5
+
6
+ import { EntityValues, StorageConfig, StorageSource, StorageSourceRegistry } from "@rebasepro/types";import { useCallback, useEffect, useMemo, useState } from "react";
7
+ import { resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource } from "@rebasepro/common";
8
+ import { useAuthController } from "../hooks";
9
+ import { useStorageSources } from "../contexts/StorageSourcesContext";
10
+ import { randomString } from "@rebasepro/utils";
11
+
12
+ export type StorageFieldSize = "smallest" | "small" | "medium" | "large" | number;
13
+
14
+ /**
15
+ * Internal representation of an item in the storage
16
+ * It can have two states, having a storagePathOrDownloadUrl set,
17
+ * which means the file has been uploaded, and it is rendered as a preview
18
+ * Or have a pending file being uploaded.
19
+ */
20
+ export interface StorageFieldItem {
21
+ id: number; // generated on the fly for internal use only
22
+ storagePathOrDownloadUrl?: string;
23
+ file?: File;
24
+ fileName?: string;
25
+ metadata?: Record<string, unknown>,
26
+ size: StorageFieldSize
27
+ }
28
+
29
+ export function useStorageUploadController<M extends Record<string, unknown>>({
30
+ entityId,
31
+ entityValues,
32
+ path,
33
+ value,
34
+ property,
35
+ propertyKey,
36
+ storageSource,
37
+ storageSourceRegistry,
38
+ disabled,
39
+ onChange
40
+ }:
41
+ {
42
+ entityId?: string | number,
43
+ entityValues: EntityValues<M>,
44
+ value: string | string[] | null;
45
+ path?: string,
46
+ propertyKey: string,
47
+ property: StringProperty | ArrayProperty | StringProperty | ArrayProperty,
48
+ storageSource: StorageSource,
49
+ /** Optional explicit registry. When omitted, the hook reads from the
50
+ * `StorageSourcesContext` provided by `<Rebase storageSources={...}>`. */
51
+ storageSourceRegistry?: StorageSourceRegistry,
52
+ disabled: boolean,
53
+ onChange: (value: string | string[] | null) => void
54
+ }) {
55
+
56
+ const authController = useAuthController();
57
+ const storageSources = useStorageSources();
58
+ const storage: StorageConfig | undefined = property.type === "string"
59
+ ? property.storage
60
+ : property.type === "array" &&
61
+ (property.of as Property).type === "string"
62
+ ? (property.of as StringProperty).storage
63
+ : undefined;
64
+
65
+ // Resolve the correct storage source for this property.
66
+ // Priority: explicit registry prop → StorageSourcesContext → default.
67
+ const resolvedStorageSource = useMemo(() => resolveStorageSource({
68
+ sourceKey: storage?.storageSource,
69
+ registry: storageSourceRegistry,
70
+ sources: storageSources.sources,
71
+ defaultSource: storageSource
72
+ }), [storage?.storageSource, storageSourceRegistry, storageSources.sources, storageSource]);
73
+
74
+ const multipleFilesSupported = property.type === "array";
75
+
76
+ if (!storage)
77
+ throw Error("Storage meta must be specified");
78
+
79
+ const processFile = storage?.processFile;
80
+
81
+ const metadata: Record<string, unknown> | undefined = storage?.metadata;
82
+ const size = multipleFilesSupported ? "medium" : "large";
83
+
84
+ const imageResize = storage?.imageResize;
85
+
86
+ const internalInitialValue: StorageFieldItem[] =
87
+ getInternalInitialValue(multipleFilesSupported, value, metadata, size);
88
+
89
+ const [initialValue, setInitialValue] = useState<string | string[] | null>(value);
90
+ const [internalValue, setInternalValue] = useState<StorageFieldItem[]>(internalInitialValue);
91
+
92
+ useEffect(() => {
93
+ if (!equal(initialValue, value)) {
94
+ setInitialValue(value);
95
+ setInternalValue(internalInitialValue);
96
+ }
97
+ }, [internalInitialValue, value, initialValue]);
98
+
99
+ const fileNameBuilder = useCallback(async (file: File) => {
100
+ if (storage.fileName) {
101
+ const fileName = await resolveStorageFilenameString({
102
+ input: storage.fileName,
103
+ storage,
104
+ values: entityValues,
105
+ entityId,
106
+ path,
107
+ property: property,
108
+ file,
109
+ propertyKey
110
+ });
111
+ if (!fileName || fileName.length === 0) {
112
+ throw Error("You need to return a valid filename");
113
+ }
114
+ return fileName;
115
+ }
116
+ return randomString() + "_" + file.name;
117
+ }, [entityId, entityValues, path, property, propertyKey, storage]);
118
+
119
+ const storagePathBuilder = useCallback((file: File) => {
120
+ return resolveStoragePathString({
121
+ input: storage.storagePath,
122
+ storage,
123
+ values: entityValues,
124
+ entityId,
125
+ path,
126
+ property: property,
127
+ file,
128
+ propertyKey
129
+ }) ?? "/";
130
+ }, [entityId, entityValues, path, property, propertyKey, storage]);
131
+
132
+ const onFileUploadComplete = useCallback(async (uploadedPath: string,
133
+ entry: StorageFieldItem,
134
+ metadata?: Record<string, unknown>,
135
+ uploadedUrl?: string) => {
136
+
137
+ console.debug("onFileUploadComplete", uploadedPath, entry);
138
+
139
+ let uploadPathOrDownloadUrl: string | null = uploadedPath;
140
+
141
+ if (storage.includeBucketUrl) {
142
+ if (!uploadedUrl) {
143
+ console.warn("includeBucketUrl is set but no fully-qualified storage URL was returned by the StorageSource. Falling back to the storage path.");
144
+ } else {
145
+ uploadPathOrDownloadUrl = uploadedUrl;
146
+ }
147
+ }
148
+
149
+ if (storage.storeUrl) {
150
+ uploadPathOrDownloadUrl = (await resolvedStorageSource.getSignedUrl(uploadedPath)).url;
151
+ }
152
+ if (storage.postProcess && uploadPathOrDownloadUrl) {
153
+ uploadPathOrDownloadUrl = await storage.postProcess(uploadPathOrDownloadUrl);
154
+ }
155
+
156
+ if (!uploadPathOrDownloadUrl) {
157
+ console.warn("uploadPathOrDownloadUrl is null")
158
+ return;
159
+ }
160
+
161
+ let newValue: StorageFieldItem[];
162
+
163
+ entry.storagePathOrDownloadUrl = uploadPathOrDownloadUrl;
164
+ entry.metadata = metadata;
165
+ newValue = [...internalValue];
166
+
167
+ newValue = removeDuplicates(newValue);
168
+ setInternalValue(newValue);
169
+
170
+ const fieldValue = newValue
171
+ .filter(e => !!e.storagePathOrDownloadUrl)
172
+ .map(e => e.storagePathOrDownloadUrl as string);
173
+
174
+ if (multipleFilesSupported) {
175
+ onChange(fieldValue);
176
+ } else {
177
+ onChange(fieldValue ? fieldValue[0] : null);
178
+ }
179
+ }, [internalValue, multipleFilesSupported, onChange, storage, resolvedStorageSource]);
180
+
181
+ const onFileUploadError = useCallback((entry: StorageFieldItem) => {
182
+ console.debug("onFileUploadError", entry);
183
+
184
+ // Remove the failed entry from internalValue
185
+ const newValue = internalValue.filter(item => item.id !== entry.id);
186
+ setInternalValue(newValue);
187
+ }, [internalValue]);
188
+
189
+ const onFilesAdded = useCallback(async (acceptedFiles: File[]) => {
190
+
191
+ if (!acceptedFiles.length || disabled)
192
+ return;
193
+
194
+ if (processFile) {
195
+ try {
196
+ acceptedFiles = await Promise.all(acceptedFiles.map(async file => {
197
+ const processedFile = await processFile(file);
198
+ if (!processedFile) {
199
+ return file;
200
+ }
201
+ return processedFile;
202
+ }));
203
+ } catch (e) {
204
+ console.error("Error processing file with custom code. Attempting to continue uploading.", e);
205
+ }
206
+ }
207
+
208
+ let newInternalValue: StorageFieldItem[];
209
+
210
+ if (multipleFilesSupported) {
211
+ newInternalValue = [...internalValue,
212
+ ...(await Promise.all(acceptedFiles.map(async file => {
213
+ if (imageResize && isImageFile(file)) {
214
+ file = await resizeImage(file, imageResize);
215
+ }
216
+
217
+ return {
218
+ id: getRandomId(),
219
+ file,
220
+ fileName: await fileNameBuilder(file),
221
+ metadata,
222
+ size
223
+ } as StorageFieldItem;
224
+ })))];
225
+ } else {
226
+ let file = acceptedFiles[0];
227
+ if (imageResize && isImageFile(file)) {
228
+ file = await resizeImage(file, imageResize);
229
+ }
230
+
231
+ newInternalValue = [{
232
+ id: getRandomId(),
233
+ file,
234
+ fileName: await fileNameBuilder(file),
235
+ metadata,
236
+ size
237
+ }];
238
+ }
239
+
240
+ // Remove either storage path or file duplicates
241
+ newInternalValue = removeDuplicates(newInternalValue);
242
+ setInternalValue(newInternalValue);
243
+ }, [disabled, fileNameBuilder, internalValue, metadata, multipleFilesSupported, size, imageResize]);
244
+
245
+ return {
246
+ internalValue,
247
+ setInternalValue,
248
+ storage,
249
+ fileNameBuilder,
250
+ storagePathBuilder,
251
+ onFileUploadComplete,
252
+ onFileUploadError,
253
+ onFilesAdded,
254
+ multipleFilesSupported,
255
+ /** The resolved StorageSource for this property — may differ from the
256
+ * default context source when `StorageConfig.storageSource` is set. */
257
+ resolvedStorageSource
258
+ }
259
+ }
260
+
261
+ function getInternalInitialValue(multipleFilesSupported: boolean,
262
+ value: string | string[] | null,
263
+ metadata: Record<string, unknown> | undefined,
264
+ size: StorageFieldSize): StorageFieldItem[] {
265
+ let strings: string[] = [];
266
+ if (multipleFilesSupported) {
267
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
268
+ strings = (value ?? []) as string[];
269
+ }
270
+ } else {
271
+ if (typeof value === "string") {
272
+ strings = value ? [value as string] : [];
273
+ }
274
+ }
275
+
276
+ return strings
277
+ .map(entry => (
278
+ {
279
+ id: getRandomId(),
280
+ storagePathOrDownloadUrl: entry,
281
+ metadata,
282
+ size
283
+ }
284
+ ));
285
+ }
286
+
287
+ function removeDuplicates(items: StorageFieldItem[]) {
288
+ return items.filter(
289
+ (item, i) => {
290
+ return ((items.map((v) => v.storagePathOrDownloadUrl).indexOf(item.storagePathOrDownloadUrl) === i) || !item.storagePathOrDownloadUrl) &&
291
+ ((items.map((v) => v.file).indexOf(item.file) === i) || !item.file);
292
+ }
293
+ );
294
+ }
295
+
296
+ function getRandomId() {
297
+ return Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER));
298
+ }
299
+
300
+ /**
301
+ * Check if a file is an image type supported for resizing
302
+ */
303
+ function isImageFile(file: File): boolean {
304
+ return file.type === "image/jpeg" ||
305
+ file.type === "image/png" ||
306
+ file.type === "image/webp";
307
+ }
308
+
309
+ /**
310
+ * Resize and compress an image using compressorjs.
311
+ */
312
+ async function resizeImage(
313
+ file: File,
314
+ imageResize?: StorageConfig["imageResize"]
315
+ ): Promise<File> {
316
+ const maxWidth = imageResize?.maxWidth;
317
+ const maxHeight = imageResize?.maxHeight;
318
+ const quality = (imageResize?.quality ?? 80) / 100;
319
+ const mode = imageResize?.mode ?? "contain";
320
+
321
+ // Determine output format
322
+ let mimeType = file.type;
323
+ if (imageResize?.format && imageResize.format !== "original") {
324
+ mimeType = `image/${imageResize.format}`;
325
+ }
326
+
327
+ return new Promise<File>((resolve, reject) => {
328
+ new Compressor(file, {
329
+ quality,
330
+ maxWidth,
331
+ maxHeight,
332
+ mimeType,
333
+ // Use cover mode if specified (crops to fit)
334
+ // Otherwise use contain mode (scales to fit)
335
+ ...(mode === "cover" || mode === undefined ? {
336
+ width: maxWidth,
337
+ height: maxHeight,
338
+ resize: "cover" as const
339
+ } : {}),
340
+ success: (result) => {
341
+ const compressedFile = new File([result], file.name, {
342
+ type: result.type,
343
+ lastModified: Date.now()
344
+ });
345
+ resolve(compressedFile);
346
+ },
347
+ error: reject
348
+ });
349
+ });
350
+ }
@@ -0,0 +1,196 @@
1
+
2
+ import path from "path";
3
+ import ts from "typescript";
4
+ import MagicString from "magic-string";
5
+
6
+ export interface RebaseCollectionsPluginOptions {
7
+ /**
8
+ * The path to the collections directory containing the schema definitions.
9
+ * Use a relative or absolute path from the root of your frontend workspace.
10
+ */
11
+ collectionsDir: string;
12
+ }
13
+
14
+ /**
15
+ * Properties on collection objects that accept `ComponentRef` values.
16
+ * When a string literal is found for any of these keys in a collection file,
17
+ * the transform plugin replaces it with a `LazyComponentRef` object so the
18
+ * component is loaded lazily and never evaluated by the backend.
19
+ */
20
+ const LAZY_COMPONENT_KEYS = new Set(["Field", "Preview", "Builder"]);
21
+
22
+ /**
23
+ * Walk a TypeScript AST node tree, invoking `visitor` for every node.
24
+ */
25
+ function walkAST(node: ts.Node, visitor: (n: ts.Node) => void): void {
26
+ visitor(node);
27
+ node.forEachChild(child => walkAST(child, visitor));
28
+ }
29
+
30
+ /**
31
+ * Return the property name text of a `PropertyAssignment` if the name is
32
+ * a plain identifier or a string literal. Returns `undefined` for computed
33
+ * property names or other exotic forms.
34
+ */
35
+ function getPropertyName(node: ts.PropertyAssignment): string | undefined {
36
+ const { name } = node;
37
+ if (ts.isIdentifier(name)) return name.text;
38
+ if (ts.isStringLiteral(name)) return name.text;
39
+ return undefined;
40
+ }
41
+
42
+ /**
43
+ * Perform the AST-based transform on `code`.
44
+ *
45
+ * Parses the source with the TypeScript compiler API, walks the tree for
46
+ * `PropertyAssignment` nodes whose name matches one of `LAZY_COMPONENT_KEYS`
47
+ * and whose initializer is a string literal starting with `./` or `../`.
48
+ *
49
+ * Each match is rewritten in-place via `magic-string` so that source-maps
50
+ * remain correct.
51
+ *
52
+ * @returns `{ code, map }` if at least one replacement was made; `null` otherwise.
53
+ */
54
+ export function transformCollectionSource(
55
+ code: string,
56
+ id: string
57
+ ): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
58
+ // Use TSX kind to handle both .ts and .tsx files uniformly.
59
+ const sourceFile = ts.createSourceFile(
60
+ id,
61
+ code,
62
+ ts.ScriptTarget.Latest,
63
+ /* setParentNodes */ true,
64
+ ts.ScriptKind.TSX
65
+ );
66
+
67
+ const ms = new MagicString(code);
68
+ let replaced = false;
69
+
70
+ walkAST(sourceFile, (node) => {
71
+ // Only look at PropertyAssignment nodes (key: value in object literals)
72
+ if (!ts.isPropertyAssignment(node)) return;
73
+
74
+ // Check the property name matches one of the lazy component keys
75
+ const propName = getPropertyName(node);
76
+ if (!propName || !LAZY_COMPONENT_KEYS.has(propName)) return;
77
+
78
+ // Check the initializer is a string literal
79
+ const init = node.initializer;
80
+ if (!ts.isStringLiteral(init)) return;
81
+
82
+ // Only transform dot-relative paths
83
+ const importPath = init.text;
84
+ if (!importPath.startsWith("./") && !importPath.startsWith("../")) return;
85
+
86
+ // Preserve the original quote character from the source
87
+ const initStart = init.getStart(sourceFile);
88
+ const quoteChar = code.charAt(initStart);
89
+
90
+ const replacement =
91
+ `{ __rebaseLazy: true, load: () => import(${quoteChar}${importPath}${quoteChar}) }`;
92
+
93
+ ms.overwrite(initStart, init.getEnd(), replacement);
94
+ replaced = true;
95
+ });
96
+
97
+ if (!replaced) return null;
98
+
99
+ return {
100
+ code: ms.toString(),
101
+ map: ms.generateMap({ hires: true })
102
+ };
103
+ }
104
+
105
+ /**
106
+ * A Vite plugin that dynamically loads and automatically wires Rebase collections.
107
+ *
108
+ * It provides two capabilities:
109
+ * 1. A **virtual module** `"virtual:rebase-collections"` that statically exports
110
+ * the resolved collections array.
111
+ * 2. A **transform hook** that converts string-based component references
112
+ * (e.g. `Field: "../../components/MyField"`) into `LazyComponentRef` objects
113
+ * (`{ __rebaseLazy: true, load: () => import(...) }`), enabling code-splitting
114
+ * and preventing the backend from loading React-dependent modules.
115
+ */
116
+ export function rebaseCollectionsPlugin(options: RebaseCollectionsPluginOptions) {
117
+ const virtualModuleId = "virtual:rebase-collections";
118
+ const resolvedVirtualModuleId = "\0" + virtualModuleId;
119
+
120
+ let resolvedCollectionsDir: string;
121
+
122
+ return {
123
+ name: "rebase-collections-plugin",
124
+
125
+ configResolved(config: { root: string }) {
126
+ // Resolve the collections directory to an absolute path
127
+ // so the `transform` hook can match files reliably.
128
+ resolvedCollectionsDir = path.isAbsolute(options.collectionsDir)
129
+ ? options.collectionsDir
130
+ : path.resolve(config.root, options.collectionsDir);
131
+ },
132
+
133
+ resolveId(id: string) {
134
+ if (id === virtualModuleId) {
135
+ return resolvedVirtualModuleId;
136
+ }
137
+ return null;
138
+ },
139
+
140
+ load(id: string) {
141
+ if (id === resolvedVirtualModuleId) {
142
+ // Vite evaluates `import.meta.glob` relative to the project root.
143
+ // We ensure it starts with '/' so Vite knows evaluating it from the root.
144
+ const globPath = options.collectionsDir.startsWith("/")
145
+ ? `${options.collectionsDir}/*.ts`
146
+ : `/${options.collectionsDir}/*.ts`;
147
+
148
+ return `
149
+ // GENERATED BY @rebasepro/app Vite Plugin
150
+ const collectionModules = import.meta.glob('${globPath}', { eager: true });
151
+
152
+ // index exports the directory's defaults, not a collection —
153
+ // it has no default export, so it drops out of the list below.
154
+ // Mirrors loadCollectionsFromDirectory in @rebasepro/server:
155
+ // the admin would otherwise show a collection as having no
156
+ // rules while the database enforces the inherited ones.
157
+ const indexEntry = Object.entries(collectionModules)
158
+ .find(([path]) => path.endsWith("/index.ts") || path.endsWith("/index.js"));
159
+ const defaultSecurityRules = indexEntry?.[1]?.defaultSecurityRules;
160
+
161
+ export const collections = Object.values(collectionModules)
162
+ .map((module) => module.default)
163
+ .filter(Boolean)
164
+ .map((collection) =>
165
+ defaultSecurityRules?.length && !collection.securityRules?.length
166
+ ? { ...collection, securityRules: defaultSecurityRules }
167
+ : collection
168
+ );
169
+ `;
170
+ }
171
+ return null;
172
+ },
173
+
174
+ /**
175
+ * Transform collection files to convert string component references
176
+ * into lazy-loading `LazyComponentRef` objects.
177
+ *
178
+ * Example transform:
179
+ * ```
180
+ * // Input
181
+ * Field: "../../frontend/src/components/MyField"
182
+ *
183
+ * // Output
184
+ * Field: { __rebaseLazy: true, load: () => import("../../frontend/src/components/MyField") }
185
+ * ```
186
+ */
187
+ transform(code: string, id: string) {
188
+ // Only process .ts/.tsx files inside the collections directory
189
+ if (!resolvedCollectionsDir) return null;
190
+ if (!id.startsWith(resolvedCollectionsDir)) return null;
191
+ if (!/\.tsx?$/.test(id)) return null;
192
+
193
+ return transformCollectionSource(code, id);
194
+ }
195
+ };
196
+ }