@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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/auth/api.d.ts +66 -0
- package/dist/auth/index.d.ts +13 -0
- package/dist/auth/types.d.ts +127 -0
- package/dist/auth/useRebaseAuthController.d.ts +13 -0
- package/dist/components/AIIcon.d.ts +17 -0
- package/dist/components/AuthSimulationMenu.d.ts +0 -0
- package/dist/components/ConfirmationDialog.d.ts +9 -0
- package/dist/components/Debug/UIReferenceView.d.ts +13 -0
- package/dist/components/Debug/UIStyleGuide.d.ts +2 -0
- package/dist/components/Debug/collection-views/CardViewDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/CollectionTableDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/KanbanBoardDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/index.d.ts +3 -0
- package/dist/components/Debug/collection-views/sample_data.d.ts +23 -0
- package/dist/components/Debug/crm-dashboard/CalendarWidget.d.ts +22 -0
- package/dist/components/Debug/crm-dashboard/CrmDashboardDemo.d.ts +2 -0
- package/dist/components/Debug/crm-dashboard/DashboardMetrics.d.ts +15 -0
- package/dist/components/Debug/crm-dashboard/PipelineOverview.d.ts +34 -0
- package/dist/components/Debug/crm-dashboard/RecentActivity.d.ts +19 -0
- package/dist/components/Debug/crm-dashboard/TaskFilters.d.ts +30 -0
- package/dist/components/Debug/crm-dashboard/TaskTable.d.ts +50 -0
- package/dist/components/Debug/crm-dashboard/TasksView.d.ts +41 -0
- package/dist/components/ErrorTooltip.d.ts +3 -0
- package/dist/components/ErrorView.d.ts +21 -0
- package/dist/components/LanguageToggle.d.ts +2 -0
- package/dist/components/LoginView/LoginView.d.ts +113 -0
- package/dist/components/LoginView/index.d.ts +2 -0
- package/dist/components/NotFoundPage.d.ts +2 -0
- package/dist/components/RebaseAuth.d.ts +10 -0
- package/dist/components/RebaseLogo.d.ts +7 -0
- package/dist/components/SchemaDriftBanner.d.ts +27 -0
- package/dist/components/UnsavedChangesDialog.d.ts +9 -0
- package/dist/components/UserDisplay.d.ts +7 -0
- package/dist/components/UserSelectPopover.d.ts +63 -0
- package/dist/components/UserSettingsView.d.ts +2 -0
- package/dist/components/common/index.d.ts +6 -0
- package/dist/components/common/table_height.d.ts +5 -0
- package/dist/components/common/types.d.ts +66 -0
- package/dist/components/common/useColumnsIds.d.ts +9 -0
- package/dist/components/common/useDataTableController.d.ts +45 -0
- package/dist/components/common/useDebouncedData.d.ts +9 -0
- package/dist/components/common/useScrollRestoration.d.ts +14 -0
- package/dist/components/index.d.ts +18 -0
- package/dist/contexts/AdminModeController.d.ts +4 -0
- package/dist/contexts/AnalyticsContext.d.ts +3 -0
- package/dist/contexts/AuthControllerContext.d.ts +3 -0
- package/dist/contexts/CollectionScopeContext.d.ts +50 -0
- package/dist/contexts/ComponentOverrideContext.d.ts +42 -0
- package/dist/contexts/CustomizationControllerContext.d.ts +3 -0
- package/dist/contexts/DataDriverContext.d.ts +3 -0
- package/dist/contexts/DataSourcesContext.d.ts +31 -0
- package/dist/contexts/DatabaseAdminContext.d.ts +3 -0
- package/dist/contexts/DialogsProvider.d.ts +4 -0
- package/dist/contexts/EffectiveRoleController.d.ts +4 -0
- package/dist/contexts/ModeController.d.ts +4 -0
- package/dist/contexts/RebaseClientInstanceContext.d.ts +6 -0
- package/dist/contexts/RebaseDataContext.d.ts +3 -0
- package/dist/contexts/SnackbarProvider.d.ts +2 -0
- package/dist/contexts/StorageSourceContext.d.ts +3 -0
- package/dist/contexts/StorageSourcesContext.d.ts +25 -0
- package/dist/contexts/UserConfigurationPersistenceContext.d.ts +3 -0
- package/dist/contexts/index.d.ts +17 -0
- package/dist/core/PluginLifecycleManager.d.ts +17 -0
- package/dist/core/PluginProviderStack.d.ts +21 -0
- package/dist/core/Rebase.d.ts +15 -0
- package/dist/core/RebaseProps.d.ts +291 -0
- package/dist/core/RebaseRouter.d.ts +4 -0
- package/dist/core/RebaseRoutes.d.ts +17 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/hooks/ApiConfigContext.d.ts +24 -0
- package/dist/hooks/data/delete.d.ts +33 -0
- package/dist/hooks/data/save.d.ts +40 -0
- package/dist/hooks/data/useCollection.d.ts +62 -0
- package/dist/hooks/data/useData.d.ts +13 -0
- package/dist/hooks/data/useDataOrder.d.ts +12 -0
- package/dist/hooks/data/useFetch.d.ts +43 -0
- package/dist/hooks/data/useRelationSelector.d.ts +52 -0
- package/dist/hooks/index.d.ts +36 -0
- package/dist/hooks/useAdminModeController.d.ts +19 -0
- package/dist/hooks/useAnalyticsController.d.ts +5 -0
- package/dist/hooks/useAuthController.d.ts +11 -0
- package/dist/hooks/useAuthSubscription.d.ts +2 -0
- package/dist/hooks/useBackendStorageSource.d.ts +30 -0
- package/dist/hooks/useBridgeRegistration.d.ts +18 -0
- package/dist/hooks/useBrowserTitleAndIcon.d.ts +6 -0
- package/dist/hooks/useBuildAdminModeController.d.ts +6 -0
- package/dist/hooks/useBuildEffectiveRoleController.d.ts +8 -0
- package/dist/hooks/useBuildLocalConfigurationPersistence.d.ts +2 -0
- package/dist/hooks/useBuildModeController.d.ts +6 -0
- package/dist/hooks/useClipboard.d.ts +57 -0
- package/dist/hooks/useCollapsedGroups.d.ts +27 -0
- package/dist/hooks/useComponentOverride.d.ts +32 -0
- package/dist/hooks/useCustomizationController.d.ts +11 -0
- package/dist/hooks/useDialogsController.d.ts +11 -0
- package/dist/hooks/useEffectiveRoleController.d.ts +7 -0
- package/dist/hooks/useLargeLayout.d.ts +1 -0
- package/dist/hooks/useModeController.d.ts +19 -0
- package/dist/hooks/usePermissions.d.ts +12 -0
- package/dist/hooks/useRebaseClient.d.ts +5 -0
- package/dist/hooks/useRebaseContext.d.ts +11 -0
- package/dist/hooks/useRebaseRegistry.d.ts +34 -0
- package/dist/hooks/useResolvedComponent.d.ts +47 -0
- package/dist/hooks/useSlot.d.ts +18 -0
- package/dist/hooks/useSnackbarController.d.ts +20 -0
- package/dist/hooks/useStorageSource.d.ts +6 -0
- package/dist/hooks/useStudioBridge.d.ts +79 -0
- package/dist/hooks/useTranslation.d.ts +17 -0
- package/dist/hooks/useUnsavedChangesDialog.d.ts +12 -0
- package/dist/hooks/useUserConfigurationPersistence.d.ts +8 -0
- package/dist/i18n/RebaseI18nProvider.d.ts +33 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.es.js +16843 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +17021 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/internal/common.d.ts +14 -0
- package/dist/internal/useRestoreScroll.d.ts +15 -0
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/en.d.ts +10 -0
- package/dist/locales/es.d.ts +10 -0
- package/dist/locales/fr.d.ts +2 -0
- package/dist/locales/hi.d.ts +2 -0
- package/dist/locales/it.d.ts +2 -0
- package/dist/locales/pt.d.ts +7 -0
- package/dist/util/constants.d.ts +1 -0
- package/dist/util/createFormexStub.d.ts +2 -0
- package/dist/util/entity_cache.d.ts +22 -0
- package/dist/util/enums.d.ts +5 -0
- package/dist/util/icon_list.d.ts +5 -0
- package/dist/util/icons.d.ts +20 -0
- package/dist/util/index.d.ts +8 -0
- package/dist/util/previews.d.ts +4 -0
- package/dist/util/useStorageUploadController.d.ts +44 -0
- package/dist/vitePlugin.d.ts +60 -0
- package/dist/vitePlugin.js +113 -0
- package/package.json +145 -0
- package/src/auth/api.ts +148 -0
- package/src/auth/index.ts +25 -0
- package/src/auth/types.ts +115 -0
- package/src/auth/useRebaseAuthController.ts +328 -0
- package/src/components/AIIcon.tsx +47 -0
- package/src/components/AuthSimulationMenu.tsx +0 -0
- package/src/components/ConfirmationDialog.tsx +48 -0
- package/src/components/Debug/UIReferenceView.tsx +958 -0
- package/src/components/Debug/UIStyleGuide.tsx +156 -0
- package/src/components/Debug/collection-views/CardViewDemo.tsx +85 -0
- package/src/components/Debug/collection-views/CollectionTableDemo.tsx +107 -0
- package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +90 -0
- package/src/components/Debug/collection-views/index.ts +3 -0
- package/src/components/Debug/collection-views/sample_data.ts +42 -0
- package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +389 -0
- package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +1102 -0
- package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +133 -0
- package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +290 -0
- package/src/components/Debug/crm-dashboard/RecentActivity.tsx +215 -0
- package/src/components/Debug/crm-dashboard/TaskFilters.tsx +184 -0
- package/src/components/Debug/crm-dashboard/TaskTable.tsx +427 -0
- package/src/components/Debug/crm-dashboard/TasksView.tsx +234 -0
- package/src/components/ErrorTooltip.tsx +12 -0
- package/src/components/ErrorView.tsx +94 -0
- package/src/components/LanguageToggle.tsx +59 -0
- package/src/components/LoginView/LoginView.tsx +932 -0
- package/src/components/LoginView/index.ts +2 -0
- package/src/components/NotFoundPage.tsx +27 -0
- package/src/components/RebaseAuth.tsx +27 -0
- package/src/components/RebaseLogo.tsx +29 -0
- package/src/components/SchemaDriftBanner.tsx +102 -0
- package/src/components/UnsavedChangesDialog.tsx +46 -0
- package/src/components/UserDisplay.tsx +54 -0
- package/src/components/UserSelectPopover.tsx +399 -0
- package/src/components/UserSettingsView.tsx +318 -0
- package/src/components/common/index.ts +6 -0
- package/src/components/common/table_height.tsx +21 -0
- package/src/components/common/types.tsx +66 -0
- package/src/components/common/useColumnsIds.tsx +222 -0
- package/src/components/common/useDataTableController.tsx +514 -0
- package/src/components/common/useDebouncedData.ts +49 -0
- package/src/components/common/useScrollRestoration.tsx +68 -0
- package/src/components/index.tsx +26 -0
- package/src/contexts/AdminModeController.tsx +11 -0
- package/src/contexts/AnalyticsContext.tsx +4 -0
- package/src/contexts/AuthControllerContext.tsx +4 -0
- package/src/contexts/CollectionScopeContext.tsx +67 -0
- package/src/contexts/ComponentOverrideContext.tsx +81 -0
- package/src/contexts/CustomizationControllerContext.tsx +4 -0
- package/src/contexts/DataDriverContext.tsx +4 -0
- package/src/contexts/DataSourcesContext.tsx +36 -0
- package/src/contexts/DatabaseAdminContext.tsx +4 -0
- package/src/contexts/DialogsProvider.tsx +56 -0
- package/src/contexts/EffectiveRoleController.tsx +12 -0
- package/src/contexts/ModeController.tsx +11 -0
- package/src/contexts/RebaseClientInstanceContext.tsx +7 -0
- package/src/contexts/RebaseDataContext.tsx +4 -0
- package/src/contexts/SnackbarProvider.tsx +15 -0
- package/src/contexts/StorageSourceContext.tsx +4 -0
- package/src/contexts/StorageSourcesContext.tsx +30 -0
- package/src/contexts/UserConfigurationPersistenceContext.tsx +4 -0
- package/src/contexts/index.ts +17 -0
- package/src/core/PluginLifecycleManager.tsx +95 -0
- package/src/core/PluginProviderStack.tsx +40 -0
- package/src/core/Rebase.tsx +439 -0
- package/src/core/RebaseProps.tsx +320 -0
- package/src/core/RebaseRouter.tsx +17 -0
- package/src/core/RebaseRoutes.tsx +24 -0
- package/src/core/index.tsx +5 -0
- package/src/hooks/ApiConfigContext.tsx +41 -0
- package/src/hooks/data/delete.ts +55 -0
- package/src/hooks/data/save.ts +77 -0
- package/src/hooks/data/useCollection.tsx +176 -0
- package/src/hooks/data/useData.tsx +18 -0
- package/src/hooks/data/useDataOrder.ts +26 -0
- package/src/hooks/data/useFetch.tsx +140 -0
- package/src/hooks/data/useRelationSelector.tsx +234 -0
- package/src/hooks/index.tsx +45 -0
- package/src/hooks/useAdminModeController.tsx +23 -0
- package/src/hooks/useAnalyticsController.tsx +8 -0
- package/src/hooks/useAuthController.tsx +14 -0
- package/src/hooks/useAuthSubscription.ts +87 -0
- package/src/hooks/useBackendStorageSource.ts +288 -0
- package/src/hooks/useBridgeRegistration.tsx +32 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +23 -0
- package/src/hooks/useBuildAdminModeController.tsx +24 -0
- package/src/hooks/useBuildEffectiveRoleController.tsx +31 -0
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +75 -0
- package/src/hooks/useBuildModeController.tsx +71 -0
- package/src/hooks/useClipboard.tsx +158 -0
- package/src/hooks/useCollapsedGroups.ts +116 -0
- package/src/hooks/useComponentOverride.tsx +59 -0
- package/src/hooks/useCustomizationController.tsx +14 -0
- package/src/hooks/useDialogsController.tsx +15 -0
- package/src/hooks/useEffectiveRoleController.tsx +10 -0
- package/src/hooks/useLargeLayout.tsx +65 -0
- package/src/hooks/useModeController.tsx +23 -0
- package/src/hooks/usePermissions.ts +44 -0
- package/src/hooks/useRebaseClient.tsx +10 -0
- package/src/hooks/useRebaseContext.tsx +83 -0
- package/src/hooks/useRebaseRegistry.tsx +91 -0
- package/src/hooks/useResolvedComponent.tsx +159 -0
- package/src/hooks/useSlot.tsx +64 -0
- package/src/hooks/useSnackbarController.tsx +52 -0
- package/src/hooks/useStorageSource.tsx +11 -0
- package/src/hooks/useStudioBridge.tsx +209 -0
- package/src/hooks/useTranslation.ts +33 -0
- package/src/hooks/useUnsavedChangesDialog.tsx +64 -0
- package/src/hooks/useUserConfigurationPersistence.tsx +11 -0
- package/src/i18n/RebaseI18nProvider.tsx +160 -0
- package/src/index.ts +25 -0
- package/src/internal/common.tsx +16 -0
- package/src/internal/useRestoreScroll.tsx +69 -0
- package/src/locales/de.ts +902 -0
- package/src/locales/en.ts +980 -0
- package/src/locales/es.ts +922 -0
- package/src/locales/fr.ts +901 -0
- package/src/locales/hi.ts +901 -0
- package/src/locales/it.ts +901 -0
- package/src/locales/pt.ts +909 -0
- package/src/util/constants.ts +7 -0
- package/src/util/createFormexStub.tsx +66 -0
- package/src/util/entity_cache.ts +239 -0
- package/src/util/enums.ts +32 -0
- package/src/util/icon_list.ts +22 -0
- package/src/util/icons.tsx +149 -0
- package/src/util/index.ts +12 -0
- package/src/util/previews.ts +105 -0
- package/src/util/useStorageUploadController.tsx +350 -0
- package/src/vitePlugin.ts +196 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { FormexController } from "@rebasepro/forms";
|
|
2
|
+
|
|
3
|
+
export function createFormexStub<T extends object>(values: T): FormexController<T> {
|
|
4
|
+
const errorMessage = "You are in a read-only context. You cannot modify the formex controller.";
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
debugId: "",
|
|
8
|
+
values,
|
|
9
|
+
initialValues: values,
|
|
10
|
+
touched: {} as Record<string, boolean>,
|
|
11
|
+
dirty: false,
|
|
12
|
+
errors: {} as Record<string, string>,
|
|
13
|
+
submitCount: 0,
|
|
14
|
+
isSubmitting: false,
|
|
15
|
+
isValidating: false,
|
|
16
|
+
version: 0,
|
|
17
|
+
canUndo: false,
|
|
18
|
+
canRedo: false,
|
|
19
|
+
|
|
20
|
+
setValues: () => {
|
|
21
|
+
throw new Error(errorMessage);
|
|
22
|
+
},
|
|
23
|
+
setTouched(touched: Record<string, boolean>): void {
|
|
24
|
+
throw new Error(errorMessage);
|
|
25
|
+
},
|
|
26
|
+
setFieldValue: () => {
|
|
27
|
+
throw new Error(errorMessage);
|
|
28
|
+
},
|
|
29
|
+
setFieldTouched: () => {
|
|
30
|
+
throw new Error(errorMessage);
|
|
31
|
+
},
|
|
32
|
+
setDirty: () => {
|
|
33
|
+
throw new Error(errorMessage);
|
|
34
|
+
},
|
|
35
|
+
setSubmitCount: () => {
|
|
36
|
+
throw new Error(errorMessage);
|
|
37
|
+
},
|
|
38
|
+
setFieldError: () => {
|
|
39
|
+
throw new Error(errorMessage);
|
|
40
|
+
},
|
|
41
|
+
handleChange: () => {
|
|
42
|
+
throw new Error(errorMessage);
|
|
43
|
+
},
|
|
44
|
+
handleBlur: () => {
|
|
45
|
+
throw new Error(errorMessage);
|
|
46
|
+
},
|
|
47
|
+
handleSubmit: () => {
|
|
48
|
+
throw new Error(errorMessage);
|
|
49
|
+
},
|
|
50
|
+
validate: () => {
|
|
51
|
+
throw new Error(errorMessage);
|
|
52
|
+
},
|
|
53
|
+
resetForm: () => {
|
|
54
|
+
throw new Error(errorMessage);
|
|
55
|
+
},
|
|
56
|
+
setSubmitting: () => {
|
|
57
|
+
throw new Error(errorMessage);
|
|
58
|
+
},
|
|
59
|
+
undo: () => {
|
|
60
|
+
throw new Error(errorMessage);
|
|
61
|
+
},
|
|
62
|
+
redo: () => {
|
|
63
|
+
throw new Error(errorMessage);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { EntityReference, EntityRelation, GeoPoint, Vector } from "@rebasepro/types";
|
|
2
|
+
import { isObject, isPlainObject } from "@rebasepro/utils";
|
|
3
|
+
|
|
4
|
+
// Define a unique prefix for entity keys in sessionStorage to avoid key collisions
|
|
5
|
+
const LOCAL_STORAGE_PREFIX = "entity_cache::";
|
|
6
|
+
|
|
7
|
+
// In-memory cache to store entities for quick access
|
|
8
|
+
const entityCache: Map<string, object> = new Map();
|
|
9
|
+
|
|
10
|
+
// Check `sessionStorage` availability once during initialization
|
|
11
|
+
const isSessionStorageAvailable = typeof sessionStorage !== "undefined";
|
|
12
|
+
|
|
13
|
+
// Define custom replacer for JSON.stringify
|
|
14
|
+
function customReplacer(this: Record<string, unknown>, key: string): unknown {
|
|
15
|
+
|
|
16
|
+
const value = this[key];
|
|
17
|
+
|
|
18
|
+
// Handle Date objects
|
|
19
|
+
if (value instanceof Date) {
|
|
20
|
+
return {
|
|
21
|
+
__type: "Date",
|
|
22
|
+
value: value.toISOString()
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Handle EntityReference
|
|
27
|
+
if (value instanceof EntityReference) {
|
|
28
|
+
return {
|
|
29
|
+
__type: "EntityReference",
|
|
30
|
+
id: value.id,
|
|
31
|
+
path: value.path,
|
|
32
|
+
driver: value.driver,
|
|
33
|
+
databaseId: value.databaseId
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (value instanceof EntityRelation) {
|
|
38
|
+
return {
|
|
39
|
+
__type: "EntityRelation",
|
|
40
|
+
id: value.id,
|
|
41
|
+
path: value.path,
|
|
42
|
+
data: value.data
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle GeoPoint
|
|
47
|
+
if (value instanceof GeoPoint) {
|
|
48
|
+
return {
|
|
49
|
+
__type: "GeoPoint",
|
|
50
|
+
latitude: value.latitude,
|
|
51
|
+
longitude: value.longitude
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle Vector
|
|
56
|
+
if (value instanceof Vector) {
|
|
57
|
+
return {
|
|
58
|
+
__type: "Vector",
|
|
59
|
+
value: value.value
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Define custom reviver for JSON.parse
|
|
67
|
+
function customReviver(_key: string, value: unknown): unknown {
|
|
68
|
+
if (value && typeof value === "object" && "__type" in value) {
|
|
69
|
+
const record = value as Record<string, unknown>;
|
|
70
|
+
switch (record.__type) {
|
|
71
|
+
case "date":
|
|
72
|
+
case "Date":
|
|
73
|
+
return new Date(record.value as string);
|
|
74
|
+
case "reference":
|
|
75
|
+
case "EntityReference":
|
|
76
|
+
return new EntityReference({
|
|
77
|
+
id: record.id as string,
|
|
78
|
+
path: record.path as string,
|
|
79
|
+
driver: record.driver as string | undefined,
|
|
80
|
+
databaseId: record.databaseId as string | undefined
|
|
81
|
+
});
|
|
82
|
+
case "relation":
|
|
83
|
+
case "EntityRelation":
|
|
84
|
+
return new EntityRelation(record.id as string, record.path as string, record.data as Record<string, unknown> | undefined);
|
|
85
|
+
case "GeoPoint":
|
|
86
|
+
return new GeoPoint(record.latitude as number, record.longitude as number);
|
|
87
|
+
case "Vector":
|
|
88
|
+
return new Vector(record.value as number[]);
|
|
89
|
+
default:
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Initialize the in-memory cache by loading entities from `sessionStorage`
|
|
97
|
+
if (isSessionStorageAvailable) {
|
|
98
|
+
try {
|
|
99
|
+
// Iterate over all keys in sessionStorage to find those with the specified prefix
|
|
100
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
101
|
+
const fullKey = sessionStorage.key(i);
|
|
102
|
+
if (fullKey && fullKey.startsWith(LOCAL_STORAGE_PREFIX)) {
|
|
103
|
+
const path = fullKey.substring(LOCAL_STORAGE_PREFIX.length);
|
|
104
|
+
const entityString = sessionStorage.getItem(fullKey);
|
|
105
|
+
if (entityString) {
|
|
106
|
+
try {
|
|
107
|
+
const entity: object = JSON.parse(entityString, customReviver);
|
|
108
|
+
entityCache.set(path, entity);
|
|
109
|
+
} catch (parseError) {
|
|
110
|
+
console.error(
|
|
111
|
+
`Failed to parse entity for path "${path}" from sessionStorage:`,
|
|
112
|
+
parseError
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error("Error accessing sessionStorage during initialization:", error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Saves data to the in-memory cache and persists it individually in `sessionStorage`.
|
|
125
|
+
* @param path - The unique path/key for the data.
|
|
126
|
+
* @param data - The data to cache and persist.
|
|
127
|
+
*/
|
|
128
|
+
export function saveEntityToCache(path: string, data: object): void {
|
|
129
|
+
// Persist the data individually in sessionStorage
|
|
130
|
+
if (isSessionStorageAvailable) {
|
|
131
|
+
try {
|
|
132
|
+
const key = LOCAL_STORAGE_PREFIX + path;
|
|
133
|
+
|
|
134
|
+
const entityString = JSON.stringify(data, customReplacer);
|
|
135
|
+
sessionStorage.setItem(key, entityString);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(
|
|
138
|
+
`Failed to save entity for path "${path}" to sessionStorage:`,
|
|
139
|
+
error
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function removeEntityFromMemoryCache(path: string): void {
|
|
146
|
+
entityCache.delete(path);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function saveEntityToMemoryCache(path: string, data: object): void {
|
|
150
|
+
entityCache.set(path, data);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function getEntityFromMemoryCache(path: string): object | undefined {
|
|
154
|
+
return entityCache.get(path);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Retrieves a entity from the in-memory cache or `sessionStorage`.
|
|
160
|
+
* If the entity is not in the cache but exists in `sessionStorage`, it loads it into the cache.
|
|
161
|
+
* @param path - The unique path/key for the entity.
|
|
162
|
+
* @returns The cached entity or `undefined` if not found.
|
|
163
|
+
*/
|
|
164
|
+
export function getEntityFromCache(path: string): object | undefined {
|
|
165
|
+
|
|
166
|
+
// If not in the cache, attempt to load it from sessionStorage
|
|
167
|
+
if (isSessionStorageAvailable) {
|
|
168
|
+
try {
|
|
169
|
+
const key = LOCAL_STORAGE_PREFIX + path;
|
|
170
|
+
const entityString = sessionStorage.getItem(key);
|
|
171
|
+
if (entityString) {
|
|
172
|
+
const entity: object = JSON.parse(entityString, customReviver);
|
|
173
|
+
return entity;
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error(
|
|
177
|
+
`Failed to load entity for path "${path}" from sessionStorage:`,
|
|
178
|
+
error
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Entity not found
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Removes a entity from both the in-memory cache and `sessionStorage`.
|
|
189
|
+
* @param path - The unique path/key for the entity to remove.
|
|
190
|
+
*/
|
|
191
|
+
export function removeEntityFromCache(path: string): void {
|
|
192
|
+
|
|
193
|
+
console.debug("Removing entity from cache", path);
|
|
194
|
+
|
|
195
|
+
// Remove from the in-memory cache
|
|
196
|
+
entityCache.delete(path);
|
|
197
|
+
|
|
198
|
+
// Remove from sessionStorage
|
|
199
|
+
if (isSessionStorageAvailable) {
|
|
200
|
+
try {
|
|
201
|
+
const key = LOCAL_STORAGE_PREFIX + path;
|
|
202
|
+
sessionStorage.removeItem(key);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error(
|
|
205
|
+
`Failed to remove entity for path "${path}" from sessionStorage:`,
|
|
206
|
+
error
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
export function flattenKeys(obj: Record<string, unknown> | unknown[], prefix = "", result: string[] = []): string[] {
|
|
214
|
+
|
|
215
|
+
if (isObject(obj) || Array.isArray(obj)) {
|
|
216
|
+
const plainObject = isPlainObject(obj);
|
|
217
|
+
if (!plainObject && prefix) {
|
|
218
|
+
result.push(prefix);
|
|
219
|
+
} else {
|
|
220
|
+
const record = obj as Record<string, unknown>;
|
|
221
|
+
for (const key in record) {
|
|
222
|
+
if (Object.prototype.hasOwnProperty.call(record, key)) {
|
|
223
|
+
const newKey = prefix
|
|
224
|
+
? Array.isArray(obj)
|
|
225
|
+
? `${prefix}[${key}]`
|
|
226
|
+
: `${prefix}.${key}`
|
|
227
|
+
: key;
|
|
228
|
+
const val = record[key];
|
|
229
|
+
if (isObject(val) || Array.isArray(val)) {
|
|
230
|
+
flattenKeys(val as Record<string, unknown> | unknown[], newKey, result);
|
|
231
|
+
} else {
|
|
232
|
+
result.push(newKey);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EnumValueConfig } from "@rebasepro/types";
|
|
2
|
+
import { CHIP_COLORS, ChipColorScheme, getColorSchemeForSeed } from "@rebasepro/ui";
|
|
3
|
+
import { getLabelOrConfigFrom } from "@rebasepro/common";
|
|
4
|
+
|
|
5
|
+
export function getColorScheme(enumValues: EnumValueConfig[], key: string | number): ChipColorScheme | undefined {
|
|
6
|
+
const labelOrConfig = getLabelOrConfigFrom(enumValues, key);
|
|
7
|
+
if (!labelOrConfig?.color)
|
|
8
|
+
return getColorSchemeForSeed(key.toString());
|
|
9
|
+
if (typeof labelOrConfig === "object" && "color" in labelOrConfig) {
|
|
10
|
+
if (typeof labelOrConfig.color === "string")
|
|
11
|
+
return CHIP_COLORS[labelOrConfig.color];
|
|
12
|
+
if (typeof labelOrConfig.color === "object")
|
|
13
|
+
return labelOrConfig.color;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isEnumValueDisabled(labelOrConfig?: string | EnumValueConfig) {
|
|
19
|
+
return typeof labelOrConfig === "object" && (labelOrConfig as EnumValueConfig).disabled;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildEnumLabel(
|
|
23
|
+
labelOrConfig?: string | EnumValueConfig
|
|
24
|
+
): string | undefined {
|
|
25
|
+
if (labelOrConfig === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (typeof labelOrConfig === "object") {
|
|
28
|
+
return labelOrConfig.label;
|
|
29
|
+
} else {
|
|
30
|
+
return labelOrConfig;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const iconSynonyms: Record<string, string> = {};
|
|
2
|
+
import { iconKeys } from "@rebasepro/ui";
|
|
3
|
+
import Fuse from "fuse.js";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const map = iconKeys
|
|
7
|
+
.map((importName) => {
|
|
8
|
+
const iconSynonym = importName in iconSynonyms ? iconSynonyms[importName] : "";
|
|
9
|
+
return {
|
|
10
|
+
key: importName,
|
|
11
|
+
synonyms: iconSynonym
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
export const iconsSearch = new Fuse(map, {
|
|
15
|
+
isCaseSensitive: false,
|
|
16
|
+
shouldSort: true,
|
|
17
|
+
ignoreLocation: true,
|
|
18
|
+
distance: 0,
|
|
19
|
+
keys: ["key", "synonyms"]
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
cls,
|
|
4
|
+
colorClassesMapping,
|
|
5
|
+
coolIconKeys,
|
|
6
|
+
IconColor,
|
|
7
|
+
iconKeys,
|
|
8
|
+
iconSize,
|
|
9
|
+
lucideIcons
|
|
10
|
+
} from "@rebasepro/ui";
|
|
11
|
+
import { deepEqual as equal } from "fast-equals"
|
|
12
|
+
import { hashString, slugify } from "@rebasepro/utils";
|
|
13
|
+
|
|
14
|
+
// iconKeys are now PascalCase strings (e.g. "ShoppingCart", "Users")
|
|
15
|
+
const iconKeysMap: Record<string, string> = iconKeys.reduce((acc: Record<string, string>, key) => {
|
|
16
|
+
// Also add lowercase versions and snake_case versions to support legacy lookups
|
|
17
|
+
const lower = key.toLowerCase();
|
|
18
|
+
const snake = key.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
19
|
+
acc[lower] = key;
|
|
20
|
+
acc[snake] = key;
|
|
21
|
+
acc[key] = key;
|
|
22
|
+
return acc;
|
|
23
|
+
}, {});
|
|
24
|
+
|
|
25
|
+
function toPascalCase(str: string): string {
|
|
26
|
+
return str.split(/[-_]/).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a Lucide icon component by string key.
|
|
31
|
+
* Tries direct match, PascalCase conversion, and falls back to CircleAlert.
|
|
32
|
+
*/
|
|
33
|
+
function resolveIcon(iconKey: string): React.ComponentType<{ size?: number; className?: string }> | null {
|
|
34
|
+
const iconsMap = lucideIcons as Record<string, React.ComponentType<{ size?: number; className?: string }> | undefined>;
|
|
35
|
+
let icon = iconsMap[iconKey];
|
|
36
|
+
if (!icon) {
|
|
37
|
+
icon = iconsMap[toPascalCase(iconKey)];
|
|
38
|
+
}
|
|
39
|
+
if (!icon) {
|
|
40
|
+
icon = iconsMap.CircleAlert;
|
|
41
|
+
}
|
|
42
|
+
return icon ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render an icon element from a string key or existing React element.
|
|
47
|
+
* This resolves the icon from the lucide-react icons map directly.
|
|
48
|
+
*/
|
|
49
|
+
export function getIcon(iconKey?: string | React.ReactNode,
|
|
50
|
+
className?: string,
|
|
51
|
+
color?: IconColor,
|
|
52
|
+
size?: "smallest" | "small" | "medium" | "large" | number): React.ReactElement | undefined {
|
|
53
|
+
|
|
54
|
+
if (React.isValidElement(iconKey)) {
|
|
55
|
+
return iconKey;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!iconKey) return undefined;
|
|
59
|
+
if (typeof iconKey === "string") {
|
|
60
|
+
const lowerKey = iconKey.toLowerCase();
|
|
61
|
+
const slugifiedKey = slugify(iconKey).replace(/-/g, "_");
|
|
62
|
+
|
|
63
|
+
const mappedKey = iconKeysMap[iconKey] || iconKeysMap[lowerKey] || iconKeysMap[slugifiedKey];
|
|
64
|
+
|
|
65
|
+
if (!mappedKey) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const LucideIcon = resolveIcon(mappedKey);
|
|
70
|
+
if (!LucideIcon) return undefined;
|
|
71
|
+
|
|
72
|
+
const sizeInPx = typeof size === "number" ? size : iconSize[size ?? "medium"];
|
|
73
|
+
return <LucideIcon
|
|
74
|
+
size={sizeInPx}
|
|
75
|
+
className={cls(
|
|
76
|
+
color ? colorClassesMapping[color] : "",
|
|
77
|
+
"select-none shrink-0",
|
|
78
|
+
className
|
|
79
|
+
)}
|
|
80
|
+
/>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.warn("Invalid icon key provided:", iconKey);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type IconViewProps = {
|
|
88
|
+
slug: string;
|
|
89
|
+
name: string;
|
|
90
|
+
singularName?: string;
|
|
91
|
+
group?: string;
|
|
92
|
+
icon?: string | React.ReactNode;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const IconForView = React.memo(
|
|
96
|
+
function IconForView({
|
|
97
|
+
collectionOrView,
|
|
98
|
+
className,
|
|
99
|
+
color,
|
|
100
|
+
size = "medium"
|
|
101
|
+
}: {
|
|
102
|
+
collectionOrView?: IconViewProps,
|
|
103
|
+
color?: IconColor,
|
|
104
|
+
className?: string,
|
|
105
|
+
size?: "smallest" | "small" | "medium" | "large" | number,
|
|
106
|
+
}): React.ReactElement {
|
|
107
|
+
if (!collectionOrView) return <></>;
|
|
108
|
+
const icon = getIcon(collectionOrView.icon, className, color, size);
|
|
109
|
+
if (collectionOrView?.icon && icon)
|
|
110
|
+
return icon;
|
|
111
|
+
|
|
112
|
+
let pathname = slugify(("singularName" in collectionOrView ? collectionOrView.singularName : undefined) ?? collectionOrView.name).replace(/-/g, "_");
|
|
113
|
+
|
|
114
|
+
let key: string | undefined;
|
|
115
|
+
if (pathname in iconKeysMap)
|
|
116
|
+
key = iconKeysMap[pathname];
|
|
117
|
+
|
|
118
|
+
if (!key) {
|
|
119
|
+
pathname = slugify(collectionOrView.slug).replace(/-/g, "_");
|
|
120
|
+
if (pathname in iconKeysMap)
|
|
121
|
+
key = iconKeysMap[pathname];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const iconsCount = coolIconKeys.length;
|
|
125
|
+
|
|
126
|
+
if (!key)
|
|
127
|
+
key = coolIconKeys[hashString(collectionOrView.slug) % iconsCount];
|
|
128
|
+
|
|
129
|
+
const LucideIcon = resolveIcon(key);
|
|
130
|
+
if (!LucideIcon) return <></>;
|
|
131
|
+
|
|
132
|
+
const sizeInPx = typeof size === "number" ? size : iconSize[size];
|
|
133
|
+
return <LucideIcon
|
|
134
|
+
size={sizeInPx}
|
|
135
|
+
className={cls(
|
|
136
|
+
color ? colorClassesMapping[color] : "",
|
|
137
|
+
"select-none shrink-0",
|
|
138
|
+
className
|
|
139
|
+
)}
|
|
140
|
+
/>;
|
|
141
|
+
}, (prevProps, nextProps) => {
|
|
142
|
+
return equal(prevProps.collectionOrView?.icon, nextProps.collectionOrView?.icon) &&
|
|
143
|
+
prevProps.collectionOrView?.name === nextProps.collectionOrView?.name &&
|
|
144
|
+
prevProps.collectionOrView?.slug === nextProps.collectionOrView?.slug &&
|
|
145
|
+
prevProps.collectionOrView?.singularName === nextProps.collectionOrView?.singularName &&
|
|
146
|
+
equal(prevProps.color, nextProps.color) &&
|
|
147
|
+
prevProps.className === nextProps.className &&
|
|
148
|
+
prevProps.size === nextProps.size;
|
|
149
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
export * from "./icon_list";
|
|
3
|
+
|
|
4
|
+
export * from "./icons";
|
|
5
|
+
export * from "./createFormexStub";
|
|
6
|
+
export * from "./entity_cache";
|
|
7
|
+
|
|
8
|
+
export * from "./useStorageUploadController";
|
|
9
|
+
|
|
10
|
+
export * from "./previews";
|
|
11
|
+
export * from "./enums";
|
|
12
|
+
export * from "./constants";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { CollectionConfig, Property, PropertyConfig } from "@rebasepro/types";
|
|
2
|
+
import { AuthController } from "@rebasepro/types";
|
|
3
|
+
import { isPropertyBuilder } from "@rebasepro/common";
|
|
4
|
+
|
|
5
|
+
function isReferenceProperty(property: Property) {
|
|
6
|
+
if (!property) return null;
|
|
7
|
+
if (property.type === "reference") return true;
|
|
8
|
+
if (property.type === "array") {
|
|
9
|
+
if (Array.isArray(property.of)) return false;
|
|
10
|
+
else return property.of?.type === "reference";
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isRelationProperty(property: Property) {
|
|
16
|
+
if (!property) return null;
|
|
17
|
+
if (property.type === "relation") return true;
|
|
18
|
+
if (property.type === "array") {
|
|
19
|
+
if (Array.isArray(property.of)) return false;
|
|
20
|
+
else return property.of?.type === "relation";
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isHiddenProperty(property: Property | undefined): boolean {
|
|
26
|
+
if (!property) return false;
|
|
27
|
+
return Boolean(property.ui?.hideFromCollection);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns true when the property holds file-storage content (single image,
|
|
32
|
+
* array of images, generic upload, …). These properties are rendered by the
|
|
33
|
+
* dedicated image-slot and should NOT appear as regular preview columns.
|
|
34
|
+
*/
|
|
35
|
+
function isStorageProperty(property: Property | undefined): boolean {
|
|
36
|
+
if (!property) return false;
|
|
37
|
+
// Single string with storage config
|
|
38
|
+
if (property.type === "string" && property.storage) return true;
|
|
39
|
+
// String displayed as image URL
|
|
40
|
+
if (property.type === "string" && property.ui?.url === "image") return true;
|
|
41
|
+
// Array whose inner element has storage config or image URL
|
|
42
|
+
if (property.type === "array" && property.of && !Array.isArray(property.of)) {
|
|
43
|
+
const inner = property.of;
|
|
44
|
+
if (inner.type === "string" && (inner.storage || inner.ui?.url === "image")) return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getEntityPreviewKeys(
|
|
50
|
+
authController: AuthController,
|
|
51
|
+
targetCollection: CollectionConfig<any>,
|
|
52
|
+
fields: Record<string, PropertyConfig>,
|
|
53
|
+
previewProperties?: string[],
|
|
54
|
+
limit = 3) {
|
|
55
|
+
const allProperties = Object.keys(targetCollection.properties);
|
|
56
|
+
let listProperties = previewProperties?.filter(p => allProperties.includes(p as string));
|
|
57
|
+
if (!listProperties && targetCollection.previewProperties) {
|
|
58
|
+
listProperties = targetCollection.previewProperties?.filter(p => allProperties.includes(p as string));
|
|
59
|
+
}
|
|
60
|
+
if (listProperties && listProperties.length > 0) {
|
|
61
|
+
return listProperties;
|
|
62
|
+
} else {
|
|
63
|
+
listProperties = (targetCollection.propertiesOrder as string[]) || allProperties;
|
|
64
|
+
return listProperties
|
|
65
|
+
.filter(key => {
|
|
66
|
+
const prop = targetCollection.properties[key];
|
|
67
|
+
const isIdProp = prop && typeof prop === "object" && "isId" in prop && Boolean((prop as { isId?: boolean }).isId);
|
|
68
|
+
return !isIdProp && key !== "id";
|
|
69
|
+
})
|
|
70
|
+
.filter(key => {
|
|
71
|
+
const property = targetCollection.properties[key];
|
|
72
|
+
return property && !isPropertyBuilder(property) && !isReferenceProperty(property) && !isRelationProperty(property) && !isHiddenProperty(property) && !isStorageProperty(property as Property);
|
|
73
|
+
}).slice(0, limit);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getEntityTitlePropertyKey<M extends Record<string, any>>(collection: CollectionConfig<M>, propertyConfigs: Record<string, PropertyConfig>): string | undefined {
|
|
78
|
+
if (collection.titleProperty) {
|
|
79
|
+
return collection.titleProperty as string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const orderToSearch = (collection.propertiesOrder as string[]) || Object.keys(collection.properties);
|
|
83
|
+
let firstStringCandidate: string | undefined;
|
|
84
|
+
|
|
85
|
+
for (const key of orderToSearch) {
|
|
86
|
+
const property = collection.properties[key];
|
|
87
|
+
if (property && !isPropertyBuilder(property)) {
|
|
88
|
+
const prop = property as Property;
|
|
89
|
+
if (isHiddenProperty(prop)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (prop.type === "string" && !prop.ui?.multiline && !prop.ui?.markdown && !prop.storage && !prop.isId) {
|
|
93
|
+
if (!firstStringCandidate) {
|
|
94
|
+
firstStringCandidate = key;
|
|
95
|
+
}
|
|
96
|
+
const lowerKey = key.toLowerCase();
|
|
97
|
+
if (["name", "title", "label", "displayname", "username"].includes(lowerKey)) {
|
|
98
|
+
return key; // Immediate return if it's a strong title candidate
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return firstStringCandidate;
|
|
104
|
+
}
|
|
105
|
+
|