@rebasepro/app 0.17.3 → 0.18.0

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,127 +0,0 @@
1
- import type { Property } from "@rebasepro/types";
2
- import type { PropertyConfig, AdminCollection } from "@rebasepro/cms-types";
3
- import { AuthController } from "@rebasepro/cms-types";
4
- import { isPropertyBuilder } from "@rebasepro/common";
5
- import { getTitlePropertyKey } from "@rebasepro/app";
6
- import { SUMMARY_RANK, isStorageProperty, rankSummaryProperty } from "../collections/summary-property";
7
-
8
- function isReferenceProperty(property: Property) {
9
- if (!property) return null;
10
- if (property.type === "reference") return true;
11
- if (property.type === "array") {
12
- if (Array.isArray(property.of)) return false;
13
- else return property.of?.type === "reference";
14
- }
15
- return false;
16
- }
17
-
18
- function isRelationProperty(property: Property) {
19
- if (!property) return null;
20
- if (property.type === "relation") return true;
21
- if (property.type === "array") {
22
- if (Array.isArray(property.of)) return false;
23
- else return property.of?.type === "relation";
24
- }
25
- return false;
26
- }
27
-
28
- function isHiddenProperty(property: Property | undefined): boolean {
29
- if (!property) return false;
30
- return Boolean(property.admin?.hideFromCollection);
31
- }
32
-
33
- /**
34
- * The properties a preview surface should render for a record, best first.
35
- *
36
- * Three things decide the answer, in this order:
37
- *
38
- * 1. `previewProperties` — passed in, or declared on the collection. A stated
39
- * list is returned verbatim, ranking and limit included: a developer who
40
- * asks for the Markdown biography gets the Markdown biography.
41
- * 2. Whether the value has a one-line form at all. A map renders as a
42
- * key/value table and a Markdown field as a document; neither fits a card
43
- * line, so they never take a slot from a value that does. See
44
- * {@link rankSummaryProperty}.
45
- * 3. `propertiesOrder`, which breaks ties.
46
- *
47
- * The middle step is the one that is easy to get wrong by leaving out.
48
- * `propertiesOrder` states the *column* order of a collection table — it is
49
- * not a statement that the first three columns summarise a record, and reading
50
- * it as one is how a card ends up rendering somebody's entire biography.
51
- */
52
- export function getEntityPreviewKeys(
53
- authController: AuthController,
54
- targetCollection: AdminCollection<any>,
55
- fields: Record<string, PropertyConfig>,
56
- previewProperties?: string[],
57
- limit = 3) {
58
- const allProperties = Object.keys(targetCollection.properties);
59
- let listProperties = previewProperties?.filter(p => allProperties.includes(p as string));
60
- if (!listProperties && targetCollection.previewProperties) {
61
- listProperties = targetCollection.previewProperties?.filter(p => allProperties.includes(p as string));
62
- }
63
- if (listProperties && listProperties.length > 0) {
64
- return listProperties;
65
- }
66
-
67
- // Relations read as a link to somewhere else rather than as a value here,
68
- // so they are left to the dedicated relation chips — unless the collection
69
- // ordered its properties explicitly, which puts them back in play.
70
- const hasExplicitOrder = !!(targetCollection.propertiesOrder as string[] | undefined);
71
- const order = (targetCollection.propertiesOrder as string[]) || allProperties;
72
-
73
- const direct: string[] = [];
74
- const excerpt: string[] = [];
75
-
76
- for (const key of order) {
77
- const property = targetCollection.properties[key];
78
- if (!property || isPropertyBuilder(property)) continue;
79
- if (key === "id" || ("isId" in property && Boolean((property as { isId?: boolean }).isId))) continue;
80
- if (isReferenceProperty(property)) continue;
81
- if (!hasExplicitOrder && isRelationProperty(property)) continue;
82
- if (isHiddenProperty(property)) continue;
83
- if (isStorageProperty(property as Property)) continue;
84
-
85
- const rank = rankSummaryProperty(property as Property);
86
- if (rank === SUMMARY_RANK.UNUSABLE) continue;
87
- (rank === SUMMARY_RANK.EXCERPT ? excerpt : direct).push(key);
88
-
89
- // Every remaining slot can already be filled by a value that fits
90
- // whole, so nothing below can change the answer.
91
- if (direct.length >= limit) break;
92
- }
93
-
94
- // A long-text field is a poor summary but a fine last resort: better an
95
- // opening line than a card with nothing under the title.
96
- return [...direct, ...excerpt].slice(0, limit);
97
- }
98
-
99
- // Returned as-is rather than rebuilt per call, so the result is referentially
100
- // stable and safe to use as a hook dependency.
101
- const INCLUDE_ALL_RELATIONS: string[] = ["*"];
102
-
103
- /**
104
- * The `include` params that eager-load a collection's relations in the same
105
- * request as its rows, so previews never fetch once per relation cell.
106
- *
107
- * Only the REST transport reads `include`; the realtime transport embeds
108
- * relation data unconditionally and ignores it. Passing it either way keeps a
109
- * realtime-less deployment rendering the same cells as a realtime one.
110
- */
111
- export function getRelationIncludeParams(collection: AdminCollection<any>): string[] | undefined {
112
- if (!collection.properties) return undefined;
113
- const hasRelations = Object.values(collection.properties).some(property =>
114
- property && !isPropertyBuilder(property) &&
115
- (isRelationProperty(property as Property) || isReferenceProperty(property as Property)));
116
- return hasRelations ? INCLUDE_ALL_RELATIONS : undefined;
117
- }
118
-
119
- /**
120
- * The property that fills the title slot for a collection. Ranking lives in
121
- * `@rebasepro/common`, shared with the admin package so both agree on what an
122
- * entity is called.
123
- */
124
- export function getEntityTitlePropertyKey<M extends Record<string, any>>(collection: AdminCollection<M>, propertyConfigs: Record<string, PropertyConfig>): string | undefined {
125
- return getTitlePropertyKey(collection as AdminCollection<Record<string, unknown>>);
126
- }
127
-
@@ -1,354 +0,0 @@
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
- // One preview size. A single-file field asked for "large" (a 220px
83
- // thumbnail) while a multi-file field asked for "medium" (118px), so two
84
- // upload fields in the same form reserved wildly different heights and
85
- // neither matched its own empty state.
86
- const size = "medium";
87
-
88
- const imageResize = storage?.imageResize;
89
-
90
- const internalInitialValue: StorageFieldItem[] =
91
- getInternalInitialValue(multipleFilesSupported, value, metadata, size);
92
-
93
- const [initialValue, setInitialValue] = useState<string | string[] | null>(value);
94
- const [internalValue, setInternalValue] = useState<StorageFieldItem[]>(internalInitialValue);
95
-
96
- useEffect(() => {
97
- if (!equal(initialValue, value)) {
98
- setInitialValue(value);
99
- setInternalValue(internalInitialValue);
100
- }
101
- }, [internalInitialValue, value, initialValue]);
102
-
103
- const fileNameBuilder = useCallback(async (file: File) => {
104
- if (storage.fileName) {
105
- const fileName = await resolveStorageFilenameString({
106
- input: storage.fileName,
107
- storage,
108
- values: entityValues,
109
- entityId,
110
- path,
111
- property: property,
112
- file,
113
- propertyKey
114
- });
115
- if (!fileName || fileName.length === 0) {
116
- throw Error("You need to return a valid filename");
117
- }
118
- return fileName;
119
- }
120
- return randomString() + "_" + file.name;
121
- }, [entityId, entityValues, path, property, propertyKey, storage]);
122
-
123
- const storagePathBuilder = useCallback((file: File) => {
124
- return resolveStoragePathString({
125
- input: storage.storagePath,
126
- storage,
127
- values: entityValues,
128
- entityId,
129
- path,
130
- property: property,
131
- file,
132
- propertyKey
133
- }) ?? "/";
134
- }, [entityId, entityValues, path, property, propertyKey, storage]);
135
-
136
- const onFileUploadComplete = useCallback(async (uploadedPath: string,
137
- entry: StorageFieldItem,
138
- metadata?: Record<string, unknown>,
139
- uploadedUrl?: string) => {
140
-
141
- console.debug("onFileUploadComplete", uploadedPath, entry);
142
-
143
- let uploadPathOrDownloadUrl: string | null = uploadedPath;
144
-
145
- if (storage.includeBucketUrl) {
146
- if (!uploadedUrl) {
147
- console.warn("includeBucketUrl is set but no fully-qualified storage URL was returned by the StorageSource. Falling back to the storage path.");
148
- } else {
149
- uploadPathOrDownloadUrl = uploadedUrl;
150
- }
151
- }
152
-
153
- if (storage.storeUrl) {
154
- uploadPathOrDownloadUrl = (await resolvedStorageSource.getSignedUrl(uploadedPath)).url;
155
- }
156
- if (storage.postProcess && uploadPathOrDownloadUrl) {
157
- uploadPathOrDownloadUrl = await storage.postProcess(uploadPathOrDownloadUrl);
158
- }
159
-
160
- if (!uploadPathOrDownloadUrl) {
161
- console.warn("uploadPathOrDownloadUrl is null")
162
- return;
163
- }
164
-
165
- let newValue: StorageFieldItem[];
166
-
167
- entry.storagePathOrDownloadUrl = uploadPathOrDownloadUrl;
168
- entry.metadata = metadata;
169
- newValue = [...internalValue];
170
-
171
- newValue = removeDuplicates(newValue);
172
- setInternalValue(newValue);
173
-
174
- const fieldValue = newValue
175
- .filter(e => !!e.storagePathOrDownloadUrl)
176
- .map(e => e.storagePathOrDownloadUrl as string);
177
-
178
- if (multipleFilesSupported) {
179
- onChange(fieldValue);
180
- } else {
181
- onChange(fieldValue ? fieldValue[0] : null);
182
- }
183
- }, [internalValue, multipleFilesSupported, onChange, storage, resolvedStorageSource]);
184
-
185
- const onFileUploadError = useCallback((entry: StorageFieldItem) => {
186
- console.debug("onFileUploadError", entry);
187
-
188
- // Remove the failed entry from internalValue
189
- const newValue = internalValue.filter(item => item.id !== entry.id);
190
- setInternalValue(newValue);
191
- }, [internalValue]);
192
-
193
- const onFilesAdded = useCallback(async (acceptedFiles: File[]) => {
194
-
195
- if (!acceptedFiles.length || disabled)
196
- return;
197
-
198
- if (processFile) {
199
- try {
200
- acceptedFiles = await Promise.all(acceptedFiles.map(async file => {
201
- const processedFile = await processFile(file);
202
- if (!processedFile) {
203
- return file;
204
- }
205
- return processedFile;
206
- }));
207
- } catch (e) {
208
- console.error("Error processing file with custom code. Attempting to continue uploading.", e);
209
- }
210
- }
211
-
212
- let newInternalValue: StorageFieldItem[];
213
-
214
- if (multipleFilesSupported) {
215
- newInternalValue = [...internalValue,
216
- ...(await Promise.all(acceptedFiles.map(async file => {
217
- if (imageResize && isImageFile(file)) {
218
- file = await resizeImage(file, imageResize);
219
- }
220
-
221
- return {
222
- id: getRandomId(),
223
- file,
224
- fileName: await fileNameBuilder(file),
225
- metadata,
226
- size
227
- } as StorageFieldItem;
228
- })))];
229
- } else {
230
- let file = acceptedFiles[0];
231
- if (imageResize && isImageFile(file)) {
232
- file = await resizeImage(file, imageResize);
233
- }
234
-
235
- newInternalValue = [{
236
- id: getRandomId(),
237
- file,
238
- fileName: await fileNameBuilder(file),
239
- metadata,
240
- size
241
- }];
242
- }
243
-
244
- // Remove either storage path or file duplicates
245
- newInternalValue = removeDuplicates(newInternalValue);
246
- setInternalValue(newInternalValue);
247
- }, [disabled, fileNameBuilder, internalValue, metadata, multipleFilesSupported, size, imageResize]);
248
-
249
- return {
250
- internalValue,
251
- setInternalValue,
252
- storage,
253
- fileNameBuilder,
254
- storagePathBuilder,
255
- onFileUploadComplete,
256
- onFileUploadError,
257
- onFilesAdded,
258
- multipleFilesSupported,
259
- /** The resolved StorageSource for this property — may differ from the
260
- * default context source when `StorageConfig.storageSource` is set. */
261
- resolvedStorageSource
262
- }
263
- }
264
-
265
- function getInternalInitialValue(multipleFilesSupported: boolean,
266
- value: string | string[] | null,
267
- metadata: Record<string, unknown> | undefined,
268
- size: StorageFieldSize): StorageFieldItem[] {
269
- let strings: string[] = [];
270
- if (multipleFilesSupported) {
271
- if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
272
- strings = (value ?? []) as string[];
273
- }
274
- } else {
275
- if (typeof value === "string") {
276
- strings = value ? [value as string] : [];
277
- }
278
- }
279
-
280
- return strings
281
- .map(entry => (
282
- {
283
- id: getRandomId(),
284
- storagePathOrDownloadUrl: entry,
285
- metadata,
286
- size
287
- }
288
- ));
289
- }
290
-
291
- function removeDuplicates(items: StorageFieldItem[]) {
292
- return items.filter(
293
- (item, i) => {
294
- return ((items.map((v) => v.storagePathOrDownloadUrl).indexOf(item.storagePathOrDownloadUrl) === i) || !item.storagePathOrDownloadUrl) &&
295
- ((items.map((v) => v.file).indexOf(item.file) === i) || !item.file);
296
- }
297
- );
298
- }
299
-
300
- function getRandomId() {
301
- return Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER));
302
- }
303
-
304
- /**
305
- * Check if a file is an image type supported for resizing
306
- */
307
- function isImageFile(file: File): boolean {
308
- return file.type === "image/jpeg" ||
309
- file.type === "image/png" ||
310
- file.type === "image/webp";
311
- }
312
-
313
- /**
314
- * Resize and compress an image using compressorjs.
315
- */
316
- async function resizeImage(
317
- file: File,
318
- imageResize?: StorageConfig["imageResize"]
319
- ): Promise<File> {
320
- const maxWidth = imageResize?.maxWidth;
321
- const maxHeight = imageResize?.maxHeight;
322
- const quality = (imageResize?.quality ?? 80) / 100;
323
- const mode = imageResize?.mode ?? "contain";
324
-
325
- // Determine output format
326
- let mimeType = file.type;
327
- if (imageResize?.format && imageResize.format !== "original") {
328
- mimeType = `image/${imageResize.format}`;
329
- }
330
-
331
- return new Promise<File>((resolve, reject) => {
332
- new Compressor(file, {
333
- quality,
334
- maxWidth,
335
- maxHeight,
336
- mimeType,
337
- // Use cover mode if specified (crops to fit)
338
- // Otherwise use contain mode (scales to fit)
339
- ...(mode === "cover" || mode === undefined ? {
340
- width: maxWidth,
341
- height: maxHeight,
342
- resize: "cover" as const
343
- } : {}),
344
- success: (result) => {
345
- const compressedFile = new File([result], file.name, {
346
- type: result.type,
347
- lastModified: Date.now()
348
- });
349
- resolve(compressedFile);
350
- },
351
- error: reject
352
- });
353
- });
354
- }
package/src/vitePlugin.ts DELETED
@@ -1,215 +0,0 @@
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
- // `Filter` was missing: AdminPropertyOptions.Filter is a ComponentRef like the others, so
21
- // a string path there was left as a string and the resolver logged "raw string
22
- // ComponentRef at runtime" and rendered nothing. Key-name based, so nesting
23
- // presentation under `admin` needs no change here.
24
- const LAZY_COMPONENT_KEYS = new Set(["Field", "Preview", "Builder", "Filter"]);
25
-
26
- /**
27
- * Walk a TypeScript AST node tree, invoking `visitor` for every node.
28
- */
29
- function walkAST(node: ts.Node, visitor: (n: ts.Node) => void): void {
30
- visitor(node);
31
- node.forEachChild(child => walkAST(child, visitor));
32
- }
33
-
34
- /**
35
- * Return the property name text of a `PropertyAssignment` if the name is
36
- * a plain identifier or a string literal. Returns `undefined` for computed
37
- * property names or other exotic forms.
38
- */
39
- function getPropertyName(node: ts.PropertyAssignment): string | undefined {
40
- const { name } = node;
41
- if (ts.isIdentifier(name)) return name.text;
42
- if (ts.isStringLiteral(name)) return name.text;
43
- return undefined;
44
- }
45
-
46
- /**
47
- * Perform the AST-based transform on `code`.
48
- *
49
- * Parses the source with the TypeScript compiler API, walks the tree for
50
- * `PropertyAssignment` nodes whose name matches one of `LAZY_COMPONENT_KEYS`
51
- * and whose initializer is a string literal starting with `./` or `../`.
52
- *
53
- * Each match is rewritten in-place via `magic-string` so that source-maps
54
- * remain correct.
55
- *
56
- * @returns `{ code, map }` if at least one replacement was made; `null` otherwise.
57
- */
58
- export function transformCollectionSource(
59
- code: string,
60
- id: string
61
- ): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
62
- // Use TSX kind to handle both .ts and .tsx files uniformly.
63
- const sourceFile = ts.createSourceFile(
64
- id,
65
- code,
66
- ts.ScriptTarget.Latest,
67
- /* setParentNodes */ true,
68
- ts.ScriptKind.TSX
69
- );
70
-
71
- const ms = new MagicString(code);
72
- let replaced = false;
73
-
74
- walkAST(sourceFile, (node) => {
75
- // Only look at PropertyAssignment nodes (key: value in object literals)
76
- if (!ts.isPropertyAssignment(node)) return;
77
-
78
- // Check the property name matches one of the lazy component keys
79
- const propName = getPropertyName(node);
80
- if (!propName || !LAZY_COMPONENT_KEYS.has(propName)) return;
81
-
82
- // Check the initializer is a string literal
83
- const init = node.initializer;
84
- if (!ts.isStringLiteral(init)) return;
85
-
86
- // Only transform dot-relative paths
87
- const importPath = init.text;
88
- if (!importPath.startsWith("./") && !importPath.startsWith("../")) return;
89
-
90
- // Preserve the original quote character from the source
91
- const initStart = init.getStart(sourceFile);
92
- const quoteChar = code.charAt(initStart);
93
-
94
- const replacement =
95
- `{ __rebaseLazy: true, load: () => import(${quoteChar}${importPath}${quoteChar}) }`;
96
-
97
- ms.overwrite(initStart, init.getEnd(), replacement);
98
- replaced = true;
99
- });
100
-
101
- if (!replaced) return null;
102
-
103
- return {
104
- code: ms.toString(),
105
- map: ms.generateMap({ hires: true })
106
- };
107
- }
108
-
109
- /**
110
- * A Vite plugin that dynamically loads and automatically wires Rebase collections.
111
- *
112
- * It provides two capabilities:
113
- * 1. A **virtual module** `"virtual:rebase-collections"` that statically exports
114
- * the resolved collections array.
115
- * 2. A **transform hook** that converts string-based component references
116
- * (e.g. `Field: "../../components/MyField"`) into `LazyComponentRef` objects
117
- * (`{ __rebaseLazy: true, load: () => import(...) }`), enabling code-splitting
118
- * and preventing the backend from loading React-dependent modules.
119
- */
120
- export function rebaseCollectionsPlugin(options: RebaseCollectionsPluginOptions) {
121
- const virtualModuleId = "virtual:rebase-collections";
122
- const resolvedVirtualModuleId = "\0" + virtualModuleId;
123
-
124
- let resolvedCollectionsDir: string;
125
-
126
- return {
127
- name: "rebase-collections-plugin",
128
-
129
- configResolved(config: { root: string }) {
130
- // Resolve the collections directory to an absolute path
131
- // so the `transform` hook can match files reliably.
132
- resolvedCollectionsDir = path.isAbsolute(options.collectionsDir)
133
- ? options.collectionsDir
134
- : path.resolve(config.root, options.collectionsDir);
135
- },
136
-
137
- resolveId(id: string) {
138
- if (id === virtualModuleId) {
139
- return resolvedVirtualModuleId;
140
- }
141
- return null;
142
- },
143
-
144
- load(id: string) {
145
- if (id === resolvedVirtualModuleId) {
146
- // Vite evaluates `import.meta.glob` relative to the project root.
147
- // We ensure it starts with '/' so Vite knows evaluating it from the root.
148
- const globPath = options.collectionsDir.startsWith("/")
149
- ? `${options.collectionsDir}/*.ts`
150
- : `/${options.collectionsDir}/*.ts`;
151
-
152
- return `
153
- // GENERATED BY @rebasepro/app Vite Plugin
154
- const collectionModules = import.meta.glob('${globPath}', { eager: true });
155
-
156
- // index exports the directory's defaults, not a collection —
157
- // it has no default export, so it drops out of the list below.
158
- // Mirrors loadCollectionsFromDirectory in @rebasepro/server:
159
- // the admin would otherwise show a collection as having no
160
- // rules while the database enforces the inherited ones.
161
- const indexEntry = Object.entries(collectionModules)
162
- .find(([path]) => path.endsWith("/index.ts") || path.endsWith("/index.js"));
163
- const indexModule = indexEntry?.[1];
164
- const defaultSecurityRules = indexModule?.defaultSecurityRules;
165
-
166
- // The glob hands back files in alphabetical order, which would
167
- // otherwise decide the order of the navigation groups and of the
168
- // cards inside them. If index exports a \`collections\` array, that
169
- // is the one place the developer states an order, so it wins;
170
- // collections it does not mention keep following their filename.
171
- const declaredOrder = Array.isArray(indexModule?.collections)
172
- ? indexModule.collections
173
- : [];
174
- const rankOf = (collection) => {
175
- const index = declaredOrder.indexOf(collection);
176
- return index === -1 ? Number.MAX_SAFE_INTEGER : index;
177
- };
178
-
179
- export const collections = Object.values(collectionModules)
180
- .map((module) => module.default)
181
- .filter(Boolean)
182
- .sort((a, b) => rankOf(a) - rankOf(b))
183
- .map((collection) =>
184
- defaultSecurityRules?.length && !collection.securityRules?.length
185
- ? { ...collection, securityRules: defaultSecurityRules }
186
- : collection
187
- );
188
- `;
189
- }
190
- return null;
191
- },
192
-
193
- /**
194
- * Transform collection files to convert string component references
195
- * into lazy-loading `LazyComponentRef` objects.
196
- *
197
- * Example transform:
198
- * ```
199
- * // Input
200
- * Field: "../../frontend/src/components/MyField"
201
- *
202
- * // Output
203
- * Field: { __rebaseLazy: true, load: () => import("../../frontend/src/components/MyField") }
204
- * ```
205
- */
206
- transform(code: string, id: string) {
207
- // Only process .ts/.tsx files inside the collections directory
208
- if (!resolvedCollectionsDir) return null;
209
- if (!id.startsWith(resolvedCollectionsDir)) return null;
210
- if (!/\.tsx?$/.test(id)) return null;
211
-
212
- return transformCollectionSource(code, id);
213
- }
214
- };
215
- }