@rebasepro/app 0.17.3 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/components/index.d.ts +0 -3
- package/dist/contexts/AuthControllerContext.d.ts +2 -1
- package/dist/contexts/CustomizationControllerContext.d.ts +2 -1
- package/dist/contexts/DialogsProvider.d.ts +2 -1
- package/dist/contexts/RebaseDataContext.d.ts +10 -1
- package/dist/contexts/StorageSourceContext.d.ts +11 -1
- package/dist/core/RebaseProps.d.ts +2 -12
- package/dist/debug/index.d.ts +19 -0
- package/dist/debug.js +3631 -0
- package/dist/hooks/data/delete.d.ts +16 -7
- package/dist/hooks/data/save.d.ts +18 -4
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useAdminModeController.d.ts +5 -4
- package/dist/hooks/useNavigationGroupLabel.d.ts +17 -0
- package/dist/hooks/useStudioBridge.d.ts +33 -0
- package/dist/index.es.js +2599 -6980
- package/dist/index.es.js.map +1 -1
- package/dist/vitePlugin.d.ts +22 -0
- package/dist/vitePlugin.js +40 -1
- package/package.json +42 -31
- package/src/auth/api.ts +0 -134
- package/src/auth/index.ts +0 -26
- package/src/auth/types.ts +0 -115
- package/src/auth/useRebaseAuthController.ts +0 -345
- package/src/collections/collection_view_config.ts +0 -76
- package/src/collections/entity-display-cache.ts +0 -195
- package/src/collections/entity-display.ts +0 -77
- package/src/collections/entity_image_preview.ts +0 -61
- package/src/collections/filter-operator-resolution.ts +0 -119
- package/src/collections/form-layout.ts +0 -394
- package/src/collections/index.ts +0 -13
- package/src/collections/navigation_from_path.ts +0 -123
- package/src/collections/navigation_utils.ts +0 -171
- package/src/collections/parent_references_from_path.ts +0 -59
- package/src/collections/property-path.ts +0 -30
- package/src/collections/property_presentation.ts +0 -264
- package/src/collections/summary-property.ts +0 -114
- package/src/collections/title-property.ts +0 -312
- package/src/components/AIIcon.tsx +0 -47
- package/src/components/AuthSimulationMenu.tsx +0 -0
- package/src/components/ConfirmationDialog.tsx +0 -48
- package/src/components/Debug/UIReferenceView.tsx +0 -1053
- package/src/components/Debug/UIStyleGuide.tsx +0 -159
- package/src/components/Debug/collection-views/CardViewDemo.tsx +0 -87
- package/src/components/Debug/collection-views/CollectionTableDemo.tsx +0 -107
- package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +0 -89
- package/src/components/Debug/collection-views/index.ts +0 -3
- package/src/components/Debug/collection-views/sample_data.ts +0 -44
- package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +0 -387
- package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +0 -1100
- package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +0 -131
- package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +0 -288
- package/src/components/Debug/crm-dashboard/RecentActivity.tsx +0 -213
- package/src/components/Debug/crm-dashboard/TaskFilters.tsx +0 -182
- package/src/components/Debug/crm-dashboard/TaskTable.tsx +0 -425
- package/src/components/Debug/crm-dashboard/TasksView.tsx +0 -234
- package/src/components/ErrorTooltip.tsx +0 -12
- package/src/components/ErrorView.tsx +0 -94
- package/src/components/LanguageToggle.tsx +0 -59
- package/src/components/LoginView/LoginView.tsx +0 -1235
- package/src/components/LoginView/index.ts +0 -2
- package/src/components/LoginView/oauth-redirect-flow.ts +0 -184
- package/src/components/NotFoundPage.tsx +0 -27
- package/src/components/RebaseAuth.tsx +0 -27
- package/src/components/RebaseLogo.tsx +0 -88
- package/src/components/SchemaDriftBanner.tsx +0 -102
- package/src/components/UnsavedChangesDialog.tsx +0 -46
- package/src/components/UserDisplay.tsx +0 -54
- package/src/components/UserSelectPopover.tsx +0 -399
- package/src/components/UserSettingsView.tsx +0 -318
- package/src/components/common/index.ts +0 -6
- package/src/components/common/table_height.tsx +0 -21
- package/src/components/common/types.tsx +0 -66
- package/src/components/common/useColumnsIds.tsx +0 -261
- package/src/components/common/useDataTableController.tsx +0 -571
- package/src/components/common/useDebouncedData.ts +0 -49
- package/src/components/common/useScrollRestoration.tsx +0 -68
- package/src/components/index.tsx +0 -26
- package/src/contexts/AdminModeController.tsx +0 -11
- package/src/contexts/AnalyticsContext.tsx +0 -4
- package/src/contexts/AuthControllerContext.tsx +0 -4
- package/src/contexts/CollectionResolverContext.tsx +0 -24
- package/src/contexts/CollectionScopeContext.tsx +0 -68
- package/src/contexts/ComponentOverrideContext.tsx +0 -81
- package/src/contexts/CustomizationControllerContext.tsx +0 -4
- package/src/contexts/DataDriverContext.tsx +0 -4
- package/src/contexts/DataSourcesContext.tsx +0 -36
- package/src/contexts/DatabaseAdminContext.tsx +0 -4
- package/src/contexts/DialogsProvider.tsx +0 -56
- package/src/contexts/EffectiveRoleController.tsx +0 -12
- package/src/contexts/ModeController.tsx +0 -11
- package/src/contexts/RebaseClientInstanceContext.tsx +0 -7
- package/src/contexts/RebaseDataContext.tsx +0 -4
- package/src/contexts/SnackbarProvider.tsx +0 -15
- package/src/contexts/StorageSourceContext.tsx +0 -4
- package/src/contexts/StorageSourcesContext.tsx +0 -30
- package/src/contexts/UserConfigurationPersistenceContext.tsx +0 -4
- package/src/contexts/index.ts +0 -18
- package/src/core/PluginLifecycleManager.tsx +0 -96
- package/src/core/PluginProviderStack.tsx +0 -40
- package/src/core/Rebase.tsx +0 -488
- package/src/core/RebaseProps.tsx +0 -333
- package/src/core/RebaseRouter.tsx +0 -18
- package/src/core/RebaseRoutes.tsx +0 -24
- package/src/core/index.tsx +0 -5
- package/src/hooks/ApiConfigContext.tsx +0 -83
- package/src/hooks/data/collectionQuery.ts +0 -55
- package/src/hooks/data/delete.ts +0 -56
- package/src/hooks/data/save.ts +0 -78
- package/src/hooks/data/useCollection.tsx +0 -183
- package/src/hooks/data/useData.tsx +0 -18
- package/src/hooks/data/useDataOrder.ts +0 -26
- package/src/hooks/data/useFetch.tsx +0 -156
- package/src/hooks/data/useRelationSelector.tsx +0 -280
- package/src/hooks/index.tsx +0 -45
- package/src/hooks/useAdminModeController.tsx +0 -23
- package/src/hooks/useAnalyticsController.tsx +0 -8
- package/src/hooks/useAuthController.tsx +0 -15
- package/src/hooks/useAuthSubscription.ts +0 -130
- package/src/hooks/useBackendStorageSource.ts +0 -295
- package/src/hooks/useBridgeRegistration.tsx +0 -32
- package/src/hooks/useBrowserTitleAndIcon.tsx +0 -23
- package/src/hooks/useBuildAdminModeController.tsx +0 -29
- package/src/hooks/useBuildEffectiveRoleController.tsx +0 -30
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +0 -83
- package/src/hooks/useBuildModeController.tsx +0 -73
- package/src/hooks/useClipboard.tsx +0 -158
- package/src/hooks/useCollapsedGroups.ts +0 -116
- package/src/hooks/useComponentOverride.tsx +0 -59
- package/src/hooks/useCustomizationController.tsx +0 -14
- package/src/hooks/useDialogsController.tsx +0 -15
- package/src/hooks/useEffectiveRoleController.tsx +0 -10
- package/src/hooks/useLargeLayout.tsx +0 -65
- package/src/hooks/useModeController.tsx +0 -23
- package/src/hooks/useNavigationBlocker.tsx +0 -108
- package/src/hooks/usePermissions.ts +0 -45
- package/src/hooks/useRebaseClient.tsx +0 -10
- package/src/hooks/useRebaseContext.tsx +0 -84
- package/src/hooks/useRebaseRegistry.tsx +0 -91
- package/src/hooks/useResolvedComponent.tsx +0 -195
- package/src/hooks/useSlot.tsx +0 -64
- package/src/hooks/useSnackbarController.tsx +0 -80
- package/src/hooks/useStorageSource.tsx +0 -11
- package/src/hooks/useStudioBridge.tsx +0 -258
- package/src/hooks/useTranslation.ts +0 -38
- package/src/hooks/useUnsavedChangesDialog.tsx +0 -67
- package/src/hooks/useUserConfigurationPersistence.tsx +0 -11
- package/src/i18n/RebaseI18nProvider.tsx +0 -159
- package/src/index.ts +0 -32
- package/src/internal/common.tsx +0 -28
- package/src/internal/useRestoreScroll.tsx +0 -69
- package/src/locales/de.ts +0 -941
- package/src/locales/en.ts +0 -1021
- package/src/locales/es.ts +0 -962
- package/src/locales/fr.ts +0 -941
- package/src/locales/hi.ts +0 -941
- package/src/locales/it.ts +0 -941
- package/src/locales/pt.ts +0 -949
- package/src/util/constants.ts +0 -7
- package/src/util/createFormexStub.tsx +0 -66
- package/src/util/entity_cache.ts +0 -242
- package/src/util/enums.ts +0 -34
- package/src/util/icon_list.ts +0 -22
- package/src/util/icons.tsx +0 -129
- package/src/util/index.ts +0 -13
- package/src/util/local_storage.ts +0 -66
- package/src/util/previews.ts +0 -127
- package/src/util/useStorageUploadController.tsx +0 -354
- package/src/vitePlugin.ts +0 -215
- /package/dist/{components/Debug → debug}/UIReferenceView.d.ts +0 -0
- /package/dist/{components/Debug → debug}/UIStyleGuide.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/CardViewDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/CollectionTableDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/KanbanBoardDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/index.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/sample_data.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/CalendarWidget.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/CrmDashboardDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/DashboardMetrics.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/PipelineOverview.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/RecentActivity.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TaskFilters.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TaskTable.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TasksView.d.ts +0 -0
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import type { Property } from "@rebasepro/types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Whether a property can stand in for a record in *one line*.
|
|
5
|
-
*
|
|
6
|
-
* Preview surfaces — the reference card, a list row, a board card — have a
|
|
7
|
-
* single line per property and no way to grow. That is a constraint on the
|
|
8
|
-
* *value*, not on the property's importance: a Markdown biography is the most
|
|
9
|
-
* interesting column on an author, and it is still the wrong thing to paste
|
|
10
|
-
* into a 44px card. Ranking it here means the picker can skip it without
|
|
11
|
-
* anyone having to configure their way out of a broken card.
|
|
12
|
-
*
|
|
13
|
-
* Kept beside {@link getTitlePropertyCandidates} because the two answer
|
|
14
|
-
* neighbouring questions — "what is this record called" and "what else can be
|
|
15
|
-
* said about it in a line" — from the same property schema.
|
|
16
|
-
*
|
|
17
|
-
* @group Collections
|
|
18
|
-
*/
|
|
19
|
-
export const SUMMARY_RANK = {
|
|
20
|
-
/**
|
|
21
|
-
* No single-line form exists. A map renders as a key/value table and an
|
|
22
|
-
* array of maps as a stack of them; there is no first line to take.
|
|
23
|
-
*/
|
|
24
|
-
UNUSABLE: 0,
|
|
25
|
-
/**
|
|
26
|
-
* Readable in one line, but only as an excerpt — the property holds a
|
|
27
|
-
* document, and the line is its opening. Worth showing when nothing better
|
|
28
|
-
* is available, never in preference to a value that fits whole.
|
|
29
|
-
*/
|
|
30
|
-
EXCERPT: 1,
|
|
31
|
-
/** Short and self-contained: the line *is* the value. */
|
|
32
|
-
DIRECT: 2
|
|
33
|
-
} as const;
|
|
34
|
-
|
|
35
|
-
/** One of the three summary ranks. */
|
|
36
|
-
export type SummaryRank = typeof SUMMARY_RANK[keyof typeof SUMMARY_RANK];
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* File-storage backed content (single image, array of images, generic upload…).
|
|
40
|
-
* Preview surfaces give these their own image slot, so they are never also a
|
|
41
|
-
* text line.
|
|
42
|
-
*
|
|
43
|
-
* @group Collections
|
|
44
|
-
*/
|
|
45
|
-
export function isStorageProperty(property: Property | undefined): boolean {
|
|
46
|
-
if (!property) return false;
|
|
47
|
-
if (property.type === "string" && (property.storage || property.admin?.urlPreview === "image")) return true;
|
|
48
|
-
if (property.type === "array" && property.of && !Array.isArray(property.of)) {
|
|
49
|
-
const inner = property.of;
|
|
50
|
-
if (inner.type === "string" && (inner.storage || inner.admin?.urlPreview === "image")) return true;
|
|
51
|
-
}
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* True when the property holds free text long enough to be a document rather
|
|
57
|
-
* than a value — Markdown, or an explicitly multi-line string.
|
|
58
|
-
*
|
|
59
|
-
* @group Collections
|
|
60
|
-
*/
|
|
61
|
-
export function isLongTextProperty(property: Property | undefined): boolean {
|
|
62
|
-
if (!property || property.type !== "string") return false;
|
|
63
|
-
return Boolean(property.admin?.markdown || property.admin?.multiline);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* How well a property reads as one line of a preview. See {@link SUMMARY_RANK}.
|
|
68
|
-
*
|
|
69
|
-
* Decided from the property *schema* only: no values are consulted, so the
|
|
70
|
-
* answer is stable for a collection and can be memoised per collection rather
|
|
71
|
-
* than per row.
|
|
72
|
-
*
|
|
73
|
-
* @group Collections
|
|
74
|
-
*/
|
|
75
|
-
export function rankSummaryProperty(property: Property | undefined): SummaryRank {
|
|
76
|
-
if (!property) return SUMMARY_RANK.UNUSABLE;
|
|
77
|
-
|
|
78
|
-
// Rendered by the image slot, not as text.
|
|
79
|
-
if (isStorageProperty(property)) return SUMMARY_RANK.UNUSABLE;
|
|
80
|
-
|
|
81
|
-
// A map is a table of its own, whether it declares its properties (a row
|
|
82
|
-
// per sub-property) or not (a key/value dump). Neither has a first line.
|
|
83
|
-
if (property.type === "map") return SUMMARY_RANK.UNUSABLE;
|
|
84
|
-
|
|
85
|
-
if (property.type === "array") {
|
|
86
|
-
// A tuple: several properties side by side, so there is nothing single
|
|
87
|
-
// about the line it would produce.
|
|
88
|
-
if (Array.isArray(property.of)) return SUMMARY_RANK.UNUSABLE;
|
|
89
|
-
if (!property.of) return property.oneOf ? SUMMARY_RANK.UNUSABLE : SUMMARY_RANK.DIRECT;
|
|
90
|
-
// Arrays of block-shaped things inherit the block shape.
|
|
91
|
-
const innerRank = rankSummaryProperty(property.of);
|
|
92
|
-
if (innerRank === SUMMARY_RANK.UNUSABLE) return SUMMARY_RANK.UNUSABLE;
|
|
93
|
-
// An array of documents is a stack of excerpts — worse than one
|
|
94
|
-
// excerpt, and never what a card should lead with.
|
|
95
|
-
if (innerRank === SUMMARY_RANK.EXCERPT) return SUMMARY_RANK.UNUSABLE;
|
|
96
|
-
// Short values render as a run of chips, which fits a line.
|
|
97
|
-
return SUMMARY_RANK.DIRECT;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (isLongTextProperty(property)) return SUMMARY_RANK.EXCERPT;
|
|
101
|
-
|
|
102
|
-
// Numbers, dates, booleans, enums, plain strings, and single references or
|
|
103
|
-
// relations (which collapse to one linked line) all fit as they are.
|
|
104
|
-
return SUMMARY_RANK.DIRECT;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* True when the property has any single-line form at all.
|
|
109
|
-
*
|
|
110
|
-
* @group Collections
|
|
111
|
-
*/
|
|
112
|
-
export function canSummariseProperty(property: Property | undefined): boolean {
|
|
113
|
-
return rankSummaryProperty(property) !== SUMMARY_RANK.UNUSABLE;
|
|
114
|
-
}
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import { isRelationalCollectionConfig, type Property, type Relation } from "@rebasepro/types";
|
|
2
|
-
import { generateForeignKeyName } from "@rebasepro/utils";
|
|
3
|
-
import type { AdminCollection } from "@rebasepro/cms-types";
|
|
4
|
-
import { getPrimaryKeys, isPropertyBuilder } from "@rebasepro/common";
|
|
5
|
-
import { getPropertyInPath } from "./property-path";
|
|
6
|
-
import { getDisplayPropertyKey, hasDeclaredDisplay } from "./entity-display";
|
|
7
|
-
import { isStorageProperty } from "./summary-property";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* How good a property is as the human-readable title of an entity.
|
|
11
|
-
* Higher wins; ties are broken by declaration order.
|
|
12
|
-
* `DISQUALIFIED` means the property can never be a title.
|
|
13
|
-
*/
|
|
14
|
-
const SCORE = {
|
|
15
|
-
DISQUALIFIED: -1,
|
|
16
|
-
/**
|
|
17
|
-
* Points at another entity. Only readable once the target resolves, and it
|
|
18
|
-
* renders as the raw key until then — last resort, for collections (like
|
|
19
|
-
* junction tables) that hold no text of their own.
|
|
20
|
-
*/
|
|
21
|
-
REFERENCE: 5,
|
|
22
|
-
RELATION: 10,
|
|
23
|
-
/** A user picker: resolves to a person's name, like a relation does */
|
|
24
|
-
USER: 10,
|
|
25
|
-
/** Long form text — readable, but a description, not a name */
|
|
26
|
-
LONG_TEXT: 20,
|
|
27
|
-
/** A closed set of values: shared by many rows, so it identifies nothing */
|
|
28
|
-
ENUM: 30,
|
|
29
|
-
/** Identifying, but PII and usually secondary to a name */
|
|
30
|
-
EMAIL: 45,
|
|
31
|
-
/** Any plain, short, free text field */
|
|
32
|
-
PLAIN_TEXT: 60,
|
|
33
|
-
/** Free text whose name says it holds the entity's label */
|
|
34
|
-
NAMED: 100
|
|
35
|
-
} as const;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Keys whose name states outright that the property is the entity label.
|
|
39
|
-
* This is a *bonus* on top of the structural rules — never a requirement, and
|
|
40
|
-
* never the mechanism that keeps identifiers out of the title slot.
|
|
41
|
-
*/
|
|
42
|
-
const TITLE_LIKE_KEYS = new Set([
|
|
43
|
-
"name",
|
|
44
|
-
"fullname",
|
|
45
|
-
"displayname",
|
|
46
|
-
"title",
|
|
47
|
-
"label",
|
|
48
|
-
"heading",
|
|
49
|
-
"subject",
|
|
50
|
-
"username",
|
|
51
|
-
"nickname"
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
|
-
function normalizeKey(key: string): string {
|
|
55
|
-
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isHidden(property: Property): boolean {
|
|
59
|
-
return Boolean(property.admin?.hideFromCollection);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Every column on this collection that stores a foreign key: the `localKey` of
|
|
64
|
-
* each owning relation (declared inline on a property or in `relations[]`), the
|
|
65
|
-
* source column of a junction, and the key a many-to-many joins on.
|
|
66
|
-
*
|
|
67
|
-
* These hold another entity's id, so they must never fill the title slot even
|
|
68
|
-
* though they are declared as plain strings.
|
|
69
|
-
*/
|
|
70
|
-
function getForeignKeyColumns<M extends Record<string, unknown>>(collection: AdminCollection<M>): Set<string> {
|
|
71
|
-
const keys = new Set<string>();
|
|
72
|
-
|
|
73
|
-
const addRelationKeys = (relation: Relation) => {
|
|
74
|
-
switch (relation.kind) {
|
|
75
|
-
case "belongsTo":
|
|
76
|
-
// Left to be inferred, the column still takes the conventional
|
|
77
|
-
// `<relation>_id` name.
|
|
78
|
-
keys.add(relation.localKey ?? generateForeignKeyName(relation.relationName ?? ""));
|
|
79
|
-
break;
|
|
80
|
-
case "manyToMany":
|
|
81
|
-
if (relation.through?.sourceColumn) keys.add(relation.through.sourceColumn);
|
|
82
|
-
break;
|
|
83
|
-
case "via": {
|
|
84
|
-
// Only the first hop starts on this collection's table.
|
|
85
|
-
const from = relation.joinPath?.[0]?.on?.from;
|
|
86
|
-
for (const column of Array.isArray(from) ? from : [from]) {
|
|
87
|
-
if (column) keys.add(column);
|
|
88
|
-
}
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
default:
|
|
92
|
-
// hasOne / hasMany put their column on the *target*.
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
for (const relation of (isRelationalCollectionConfig(collection) ? collection.relations : undefined) ?? []) {
|
|
98
|
-
addRelationKeys(relation);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
for (const [key, propertyRaw] of Object.entries(collection.properties ?? {})) {
|
|
102
|
-
const property = propertyRaw as Property | undefined;
|
|
103
|
-
if (!property || isPropertyBuilder(property) || property.type !== "relation") continue;
|
|
104
|
-
if (property.relation) {
|
|
105
|
-
addRelationKeys({ ...property.relation,
|
|
106
|
-
relationName: property.relation.relationName ?? key });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return keys;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* True when the property is declared to hold an opaque identifier rather than
|
|
115
|
-
* something a person reads: a primary key, a foreign key, a UUID column, or a
|
|
116
|
-
* picker bound to an auth user id. Decided from the property *schema*, never
|
|
117
|
-
* from the key name.
|
|
118
|
-
*/
|
|
119
|
-
function isIdentifierProperty(property: Property, key: string, idKeys: Set<string>, foreignKeys: Set<string>): boolean {
|
|
120
|
-
if (idKeys.has(key)) return true;
|
|
121
|
-
if (foreignKeys.has(key)) return true;
|
|
122
|
-
if ("isId" in property && property.isId) return true;
|
|
123
|
-
if (property.type === "string" && property.columnType === "uuid") return true;
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function scoreTitleCandidate(property: Property, key: string, idKeys: Set<string>, foreignKeys: Set<string>): number {
|
|
128
|
-
if (isHidden(property)) return SCORE.DISQUALIFIED;
|
|
129
|
-
if (isIdentifierProperty(property, key, idKeys, foreignKeys)) return SCORE.DISQUALIFIED;
|
|
130
|
-
if (isStorageProperty(property)) return SCORE.DISQUALIFIED;
|
|
131
|
-
|
|
132
|
-
if (property.type === "relation") {
|
|
133
|
-
const isMany = property.resolvedRelation
|
|
134
|
-
? property.resolvedRelation.cardinality === "many"
|
|
135
|
-
: property.relation?.kind === "hasMany" || property.relation?.kind === "manyToMany";
|
|
136
|
-
return isMany ? SCORE.DISQUALIFIED : SCORE.RELATION;
|
|
137
|
-
}
|
|
138
|
-
if (property.type === "reference") {
|
|
139
|
-
return SCORE.REFERENCE;
|
|
140
|
-
}
|
|
141
|
-
if (property.type !== "string") {
|
|
142
|
-
// Numbers, dates, booleans, maps and arrays never read as a title.
|
|
143
|
-
return SCORE.DISQUALIFIED;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// A user picker stores an auth user id but renders the person behind it, so
|
|
147
|
-
// it ranks with relations: usable as a title, but only as a last resort.
|
|
148
|
-
if (property.userSelect) return SCORE.USER;
|
|
149
|
-
if (property.enum) return SCORE.ENUM;
|
|
150
|
-
if (property.admin?.multiline || property.admin?.markdown) return SCORE.LONG_TEXT;
|
|
151
|
-
if (property.email) return SCORE.EMAIL;
|
|
152
|
-
if (TITLE_LIKE_KEYS.has(normalizeKey(key))) return SCORE.NAMED;
|
|
153
|
-
return SCORE.PLAIN_TEXT;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* All properties that could serve as the entity title, best first.
|
|
158
|
-
*
|
|
159
|
-
* Candidates are ranked from the property *schema* — identifiers (primary
|
|
160
|
-
* keys, foreign keys, UUID columns, user pickers), images and hidden fields are
|
|
161
|
-
* excluded structurally, so a collection whose id column is called something
|
|
162
|
-
* other than `id` is handled the same as one where it isn't.
|
|
163
|
-
*
|
|
164
|
-
* When the collection declares `propertiesOrder` the developer has already
|
|
165
|
-
* stated what comes first, so qualifying candidates keep that order; otherwise
|
|
166
|
-
* they are ranked (a name beats a description beats a relation).
|
|
167
|
-
*
|
|
168
|
-
* @group Collections
|
|
169
|
-
*/
|
|
170
|
-
export function getTitlePropertyCandidates<M extends Record<string, unknown>>(
|
|
171
|
-
collection: AdminCollection<M>
|
|
172
|
-
): string[] {
|
|
173
|
-
if (!collection.properties) return [];
|
|
174
|
-
|
|
175
|
-
// `getPropertyInPath`, not `properties[key]`: a declared title on a nested
|
|
176
|
-
// field — `profile.display_name` — was looked up flat, found nothing, and
|
|
177
|
-
// silently fell through to the derived ranking. The dotted form has been
|
|
178
|
-
// documented as valid the whole time.
|
|
179
|
-
const declared = getDisplayPropertyKey(collection, "title");
|
|
180
|
-
if (declared && getPropertyInPath(collection.properties, declared)) {
|
|
181
|
-
return [declared];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const idKeys = new Set<string>(getPrimaryKeys(collection) as string[]);
|
|
185
|
-
const foreignKeys = getForeignKeyColumns(collection);
|
|
186
|
-
const explicitOrder = collection.propertiesOrder as string[] | undefined;
|
|
187
|
-
const order = explicitOrder ?? Object.keys(collection.properties);
|
|
188
|
-
|
|
189
|
-
const scored: { key: string; score: number; index: number }[] = [];
|
|
190
|
-
order.forEach((key, index) => {
|
|
191
|
-
const property = collection.properties[key];
|
|
192
|
-
if (!property || isPropertyBuilder(property)) return;
|
|
193
|
-
const score = scoreTitleCandidate(property as Property, key, idKeys, foreignKeys);
|
|
194
|
-
if (score === SCORE.DISQUALIFIED) return;
|
|
195
|
-
scored.push({ key,
|
|
196
|
-
score,
|
|
197
|
-
index });
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// An explicit `propertiesOrder` is a statement of what matters first, so it
|
|
201
|
-
// wins over the ranking — the ranking only decides between properties the
|
|
202
|
-
// developer never ordered.
|
|
203
|
-
if (!explicitOrder) {
|
|
204
|
-
scored.sort((a, b) => (b.score - a.score) || (a.index - b.index));
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return scored.map(candidate => candidate.key);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* The relation a collection *leads* with, if it leads with one.
|
|
212
|
-
*
|
|
213
|
-
* A junction-shaped collection — a pipeline entry, a membership, a booking —
|
|
214
|
-
* holds no label of its own: it is "this candidate, for that vacancy". The
|
|
215
|
-
* general ranking puts relations last (they only read once the target
|
|
216
|
-
* resolves), so the title slot would otherwise fall to whatever free text
|
|
217
|
-
* happens to follow: a note, a comment. Preview surfaces that render one row
|
|
218
|
-
* per entity (list, cards, board) prefer the leading relation instead.
|
|
219
|
-
*
|
|
220
|
-
* The decision is structural — the first property that could be a title, in the
|
|
221
|
-
* order the developer declared (or ordered) them. Declaration order is a
|
|
222
|
-
* statement about what the collection is about; property *names* are not
|
|
223
|
-
* consulted here.
|
|
224
|
-
*
|
|
225
|
-
* Returns `undefined` when that first property is anything but a
|
|
226
|
-
* single-cardinality relation, or when the collection states its own
|
|
227
|
-
* `display.title`.
|
|
228
|
-
*
|
|
229
|
-
* @group Collections
|
|
230
|
-
*/
|
|
231
|
-
export function getLeadingRelationTitleKey<M extends Record<string, unknown>>(
|
|
232
|
-
collection: AdminCollection<M>
|
|
233
|
-
): string | undefined {
|
|
234
|
-
if (!collection.properties) return undefined;
|
|
235
|
-
// The collection states what it is called, in one form or the other; either
|
|
236
|
-
// way it is not asking for the leading-relation heuristic.
|
|
237
|
-
if (hasDeclaredDisplay(collection, "title")) return undefined;
|
|
238
|
-
|
|
239
|
-
const idKeys = new Set<string>(getPrimaryKeys(collection) as string[]);
|
|
240
|
-
const foreignKeys = getForeignKeyColumns(collection);
|
|
241
|
-
const order = (collection.propertiesOrder as string[] | undefined) ?? Object.keys(collection.properties);
|
|
242
|
-
|
|
243
|
-
for (const key of order) {
|
|
244
|
-
const property = collection.properties[key];
|
|
245
|
-
if (!property || isPropertyBuilder(property)) continue;
|
|
246
|
-
if (scoreTitleCandidate(property as Property, key, idKeys, foreignKeys) === SCORE.DISQUALIFIED) continue;
|
|
247
|
-
return (property as Property).type === "relation" ? key : undefined;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* The property that should fill the title slot for a collection, ignoring any
|
|
255
|
-
* concrete values. Prefer {@link getTitlePropertyKeyForValues} when an entity
|
|
256
|
-
* is at hand — it can skip candidates that happen to be empty or to hold an id.
|
|
257
|
-
*
|
|
258
|
-
* @group Collections
|
|
259
|
-
*/
|
|
260
|
-
export function getTitlePropertyKey<M extends Record<string, unknown>>(
|
|
261
|
-
collection: AdminCollection<M>
|
|
262
|
-
): string | undefined {
|
|
263
|
-
return getTitlePropertyCandidates(collection)[0];
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
267
|
-
const LONG_HEX_REGEX = /^[0-9a-f]{24,}$/i;
|
|
268
|
-
const CUID_REGEX = /^c[a-z0-9]{20,}$/i;
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* True when a value reads as a machine identifier (UUID, ObjectId/long hex,
|
|
272
|
-
* cuid) rather than as something worth showing to a person.
|
|
273
|
-
*
|
|
274
|
-
* @group Collections
|
|
275
|
-
*/
|
|
276
|
-
export function looksLikeIdentifierValue(value: unknown): boolean {
|
|
277
|
-
if (typeof value !== "string") return false;
|
|
278
|
-
const trimmed = value.trim();
|
|
279
|
-
return UUID_REGEX.test(trimmed) || LONG_HEX_REGEX.test(trimmed) || CUID_REGEX.test(trimmed);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* The title property for a concrete entity: the best-ranked candidate that
|
|
284
|
-
* actually carries a readable value. Candidates that are empty, that repeat the
|
|
285
|
-
* entity id, or that hold an opaque identifier are skipped, so a free-text
|
|
286
|
-
* column that happens to store UUIDs never ends up as the title.
|
|
287
|
-
*
|
|
288
|
-
* Returns the top-ranked candidate when none of them has a usable value, so
|
|
289
|
-
* callers can still render a placeholder for that property.
|
|
290
|
-
*
|
|
291
|
-
* @group Collections
|
|
292
|
-
*/
|
|
293
|
-
export function getTitlePropertyKeyForValues<M extends Record<string, unknown>>(
|
|
294
|
-
collection: AdminCollection<M>,
|
|
295
|
-
values: Record<string, unknown> | undefined,
|
|
296
|
-
entityId?: string | number
|
|
297
|
-
): string | undefined {
|
|
298
|
-
const candidates = getTitlePropertyCandidates(collection);
|
|
299
|
-
if (!values || candidates.length === 0) return candidates[0];
|
|
300
|
-
|
|
301
|
-
for (const key of candidates) {
|
|
302
|
-
const value = values[key];
|
|
303
|
-
if (value === undefined || value === null || value === "") continue;
|
|
304
|
-
if (typeof value === "string") {
|
|
305
|
-
if (looksLikeIdentifierValue(value)) continue;
|
|
306
|
-
if (entityId !== undefined && value === String(entityId)) continue;
|
|
307
|
-
}
|
|
308
|
-
return key;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return candidates[0];
|
|
312
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { Wand2Icon } from "@rebasepro/ui";
|
|
2
|
-
import React from "react";
|
|
3
|
-
|
|
4
|
-
export interface AIIconProps {
|
|
5
|
-
size?: "smallest" | "small" | "medium" | "large";
|
|
6
|
-
className?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* AI-styled AutoAwesome icon with gradient coloring.
|
|
11
|
-
* Used consistently across AI features for visual identification.
|
|
12
|
-
*/
|
|
13
|
-
export function AIIcon({ size = "small", className }: AIIconProps) {
|
|
14
|
-
const sizeMap: Record<string, number> = {
|
|
15
|
-
smallest: 16,
|
|
16
|
-
small: 20,
|
|
17
|
-
medium: 24,
|
|
18
|
-
large: 28
|
|
19
|
-
};
|
|
20
|
-
const numericSize = typeof size === "string" ? sizeMap[size] || 20 : size;
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<Wand2Icon
|
|
24
|
-
size={numericSize}
|
|
25
|
-
className={className}
|
|
26
|
-
style={{
|
|
27
|
-
background: "linear-gradient(to right, var(--color-primary), var(--color-secondary))",
|
|
28
|
-
WebkitBackgroundClip: "text",
|
|
29
|
-
WebkitTextFillColor: "transparent",
|
|
30
|
-
backgroundClip: "text"
|
|
31
|
-
}}
|
|
32
|
-
/>
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Small animated dot indicator for AI-modified elements.
|
|
38
|
-
* Shows a pulsing gradient dot.
|
|
39
|
-
*/
|
|
40
|
-
export function AIModifiedIndicator({ className }: { className?: string }) {
|
|
41
|
-
return (
|
|
42
|
-
<div
|
|
43
|
-
className={`w-2 h-2 rounded-full bg-gradient-to-r from-primary to-secondary animate-pulse ${className ?? ""}`}
|
|
44
|
-
title="AI modified"
|
|
45
|
-
/>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
File without changes
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, LoadingButton } from "@rebasepro/ui";
|
|
4
|
-
import { useTranslation } from "../hooks";
|
|
5
|
-
|
|
6
|
-
export function ConfirmationDialog({
|
|
7
|
-
open,
|
|
8
|
-
onAccept,
|
|
9
|
-
onCancel,
|
|
10
|
-
title,
|
|
11
|
-
loading,
|
|
12
|
-
body
|
|
13
|
-
}: {
|
|
14
|
-
open: boolean,
|
|
15
|
-
onAccept: () => void,
|
|
16
|
-
onCancel: () => void,
|
|
17
|
-
loading?: boolean,
|
|
18
|
-
title: React.ReactNode,
|
|
19
|
-
body?: React.ReactNode,
|
|
20
|
-
}) {
|
|
21
|
-
const { t } = useTranslation();
|
|
22
|
-
return (
|
|
23
|
-
<Dialog
|
|
24
|
-
open={open}
|
|
25
|
-
onOpenChange={(open) => !open ? onCancel() : undefined}
|
|
26
|
-
>
|
|
27
|
-
<DialogTitle variant={"h6"} className={"mb-2"}>{title}</DialogTitle>
|
|
28
|
-
<DialogContent>
|
|
29
|
-
{body}
|
|
30
|
-
</DialogContent>
|
|
31
|
-
|
|
32
|
-
<DialogActions>
|
|
33
|
-
<Button
|
|
34
|
-
variant={"text"}
|
|
35
|
-
onClick={onCancel}
|
|
36
|
-
autoFocus>{t("cancel")}</Button>
|
|
37
|
-
|
|
38
|
-
<LoadingButton
|
|
39
|
-
type="submit"
|
|
40
|
-
loading={loading}
|
|
41
|
-
onClick={onAccept}
|
|
42
|
-
autoFocus>
|
|
43
|
-
{t("ok")}
|
|
44
|
-
</LoadingButton>
|
|
45
|
-
</DialogActions>
|
|
46
|
-
</Dialog>
|
|
47
|
-
);
|
|
48
|
-
}
|