@object-ui/app-shell 11.2.0 → 11.4.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.
- package/CHANGELOG.md +562 -0
- package/README.md +23 -0
- package/dist/console/ConsoleShell.js +17 -2
- package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
- package/dist/console/home/CloudOnboardingNext.js +14 -4
- package/dist/console/home/HomePage.js +34 -7
- package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
- package/dist/console/organizations/OrganizationsPage.js +16 -7
- package/dist/hooks/useConsoleActionRuntime.js +32 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/preview/DraftChangesPanel.d.ts +3 -1
- package/dist/preview/DraftChangesPanel.js +6 -5
- package/dist/utils/deriveRelatedLists.d.ts +20 -5
- package/dist/utils/deriveRelatedLists.js +31 -13
- package/dist/utils/index.d.ts +2 -24
- package/dist/utils/index.js +14 -101
- package/dist/utils/resolveViewId.d.ts +23 -0
- package/dist/utils/resolveViewId.js +37 -0
- package/dist/utils/warnSuppressedListNav.d.ts +10 -0
- package/dist/utils/warnSuppressedListNav.js +40 -0
- package/dist/views/DashboardView.js +2 -3
- package/dist/views/InterfaceListPage.js +10 -5
- package/dist/views/ObjectView.js +65 -12
- package/dist/views/PageView.js +2 -3
- package/dist/views/RecordDetailView.js +131 -104
- package/dist/views/RecordFormPage.js +7 -1
- package/dist/views/RelatedRecordActionsBridge.d.ts +24 -0
- package/dist/views/RelatedRecordActionsBridge.js +114 -0
- package/dist/views/ReportView.js +2 -3
- package/dist/views/metadata-admin/PackagesPage.js +18 -7
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
- package/dist/views/metadata-admin/clientValidation.js +8 -2
- package/dist/views/metadata-admin/color-variant-field.d.ts +1 -12
- package/dist/views/metadata-admin/color-variant-field.js +11 -0
- package/dist/views/metadata-admin/i18n.d.ts +12 -21
- package/dist/views/metadata-admin/i18n.js +343 -2
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +25 -11
- package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
- package/dist/views/metadata-admin/permission-slice.js +70 -0
- package/dist/views/metadata-admin/previews/AppNavCanvas.js +11 -7
- package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
- package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
- package/dist/views/metadata-admin/previews/OutlineStrip.d.ts +1 -13
- package/dist/views/metadata-admin/previews/OutlineStrip.js +12 -0
- package/dist/views/metadata-admin/previews/PagePreview.js +9 -0
- package/dist/views/metadata-admin/previews/SourcePageEditor.d.ts +28 -0
- package/dist/views/metadata-admin/previews/SourcePageEditor.js +83 -0
- package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
- package/dist/views/studio-design/BuilderLanding.js +133 -0
- package/dist/views/studio-design/ObjectFormDesigner.d.ts +31 -0
- package/dist/views/studio-design/ObjectFormDesigner.js +226 -0
- package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectSettingsPanel.js +45 -0
- package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectValidationsPanel.js +78 -0
- package/dist/views/studio-design/StudioDesignSurface.d.ts +20 -0
- package/dist/views/studio-design/StudioDesignSurface.js +1306 -0
- package/dist/views/studio-design/metadataError.d.ts +23 -0
- package/dist/views/studio-design/metadataError.js +44 -0
- package/dist/views/studio-design/packages-io.d.ts +27 -0
- package/dist/views/studio-design/packages-io.js +61 -0
- package/package.json +46 -43
package/dist/utils/index.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility functions for ObjectStack Console
|
|
3
3
|
*/
|
|
4
|
+
// Re-export the unified record display-name resolver (ADR-0079) so existing
|
|
5
|
+
// importers of `@object-ui/app-shell`'s `getRecordDisplayName` /
|
|
6
|
+
// `formatRecordTitle` keep working unchanged. The implementation now lives in
|
|
7
|
+
// `@object-ui/core` (pure util, shared by every view plugin and field widget).
|
|
8
|
+
export { getRecordDisplayName, deriveTitleField, isTitleEligibleField,
|
|
9
|
+
// `formatTitleTemplate` is the new canonical name; alias it to the legacy
|
|
10
|
+
// `formatRecordTitle` export this module has always provided.
|
|
11
|
+
formatTitleTemplate as formatRecordTitle, } from '@object-ui/core';
|
|
4
12
|
export { resolveRecordFormTarget, resolveFormViewLayout, } from './recordFormNavigation';
|
|
5
13
|
export { deriveRelatedLists } from './deriveRelatedLists';
|
|
6
14
|
export { preferLocal } from './preferLocal';
|
|
@@ -32,104 +40,9 @@ export function capitalizeFirst(str) {
|
|
|
32
40
|
return str;
|
|
33
41
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
34
42
|
}
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Format a record title using the titleFormat pattern.
|
|
43
|
-
*
|
|
44
|
-
* Accepts either a legacy string template or an Expression envelope
|
|
45
|
-
* (`{ dialect: 'template', source: string }`) emitted by `@objectstack/spec`'s
|
|
46
|
-
* normalized templates. The placeholder syntax (`{field}`) is identical in both
|
|
47
|
-
* shapes; only the wrapping object is new.
|
|
48
|
-
*
|
|
49
|
-
* Empty placeholders (missing or null/empty fields) are stripped along with
|
|
50
|
-
* any orphan separator they leave behind, so a template like
|
|
51
|
-
* "{full_name} - {company}"
|
|
52
|
-
* evaluated against `{ company: "Acme" }` resolves to `"Acme"` rather than
|
|
53
|
-
* `" - Acme"`. Returns an empty string when no placeholder resolved.
|
|
54
|
-
*/
|
|
55
|
-
export function formatRecordTitle(titleFormat, record) {
|
|
56
|
-
// Normalize Expression envelope ({ dialect, source }) → raw template string.
|
|
57
|
-
const template = typeof titleFormat === 'string'
|
|
58
|
-
? titleFormat
|
|
59
|
-
: (titleFormat && typeof titleFormat === 'object' && typeof titleFormat.source === 'string')
|
|
60
|
-
? titleFormat.source
|
|
61
|
-
: undefined;
|
|
62
|
-
if (!template || !record) {
|
|
63
|
-
return record?.id || record?._id || 'Record';
|
|
64
|
-
}
|
|
65
|
-
let anyResolved = false;
|
|
66
|
-
let out = template.replace(/\{([^{}]+)\}/g, (_match, fieldName) => {
|
|
67
|
-
// Support dotted paths (e.g. `{account.name}`) for $expanded lookups.
|
|
68
|
-
const parts = String(fieldName).trim().split('.');
|
|
69
|
-
let value = record;
|
|
70
|
-
for (const p of parts) {
|
|
71
|
-
if (value == null)
|
|
72
|
-
break;
|
|
73
|
-
value = value[p];
|
|
74
|
-
}
|
|
75
|
-
// Auto-extract display name from expanded reference objects, with a
|
|
76
|
-
// Salesforce-style fallback chain.
|
|
77
|
-
if (value && typeof value === 'object') {
|
|
78
|
-
const o = value;
|
|
79
|
-
let display = o.name ?? o.full_name ?? o.display_name ?? o.label ?? o.title ?? o.subject ?? null;
|
|
80
|
-
if (display == null || (typeof display === 'string' && !display.trim())) {
|
|
81
|
-
const composite = [o.salutation, o.first_name, o.last_name]
|
|
82
|
-
.filter((p) => typeof p === 'string' && p.trim())
|
|
83
|
-
.map((p) => p.trim())
|
|
84
|
-
.join(' ');
|
|
85
|
-
if (composite)
|
|
86
|
-
display = composite;
|
|
87
|
-
else if (typeof o.email === 'string' && o.email.trim())
|
|
88
|
-
display = o.email.trim();
|
|
89
|
-
else
|
|
90
|
-
display = null;
|
|
91
|
-
}
|
|
92
|
-
value = display;
|
|
93
|
-
}
|
|
94
|
-
if (value === null || value === undefined || value === '') {
|
|
95
|
-
return EMPTY_TOKEN;
|
|
96
|
-
}
|
|
97
|
-
anyResolved = true;
|
|
98
|
-
return String(value);
|
|
99
|
-
});
|
|
100
|
-
if (!anyResolved)
|
|
101
|
-
return '';
|
|
102
|
-
// Drop separators on either side of an empty token, then any leftover
|
|
103
|
-
// tokens, then collapse runs of whitespace.
|
|
104
|
-
const sepBefore = new RegExp(`\\s*${SEPARATOR_CLASS}\\s*${EMPTY_TOKEN}`, 'g');
|
|
105
|
-
const sepAfter = new RegExp(`${EMPTY_TOKEN}\\s*${SEPARATOR_CLASS}\\s*`, 'g');
|
|
106
|
-
out = out
|
|
107
|
-
.replace(sepBefore, '')
|
|
108
|
-
.replace(sepAfter, '')
|
|
109
|
-
.replace(new RegExp(EMPTY_TOKEN, 'g'), '')
|
|
110
|
-
.replace(/\s+/g, ' ')
|
|
111
|
-
.trim();
|
|
112
|
-
return out;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Get display name for a record using titleFormat or fallback
|
|
116
|
-
* @param objectDef Object definition with optional titleFormat
|
|
117
|
-
* @param record The record data
|
|
118
|
-
* @returns Display name for the record
|
|
119
|
-
*/
|
|
120
|
-
export function getRecordDisplayName(objectDef, record) {
|
|
121
|
-
if (objectDef?.titleFormat) {
|
|
122
|
-
const formatted = formatRecordTitle(objectDef.titleFormat, record);
|
|
123
|
-
if (formatted)
|
|
124
|
-
return formatted;
|
|
125
|
-
}
|
|
126
|
-
return (record?.name ||
|
|
127
|
-
record?.full_name ||
|
|
128
|
-
record?.fullName ||
|
|
129
|
-
record?.title ||
|
|
130
|
-
record?.label ||
|
|
131
|
-
record?.subject ||
|
|
132
|
-
record?.id ||
|
|
133
|
-
record?._id ||
|
|
134
|
-
'Untitled');
|
|
135
|
-
}
|
|
43
|
+
// NOTE (ADR-0079): `formatRecordTitle` (now canonically `formatTitleTemplate`)
|
|
44
|
+
// and `getRecordDisplayName` moved to `@object-ui/core` and are re-exported
|
|
45
|
+
// from the top of this module. The previous local copies — a titleFormat-only
|
|
46
|
+
// resolver that fell back to a hard-coded `name`/`title`/… list and bottomed
|
|
47
|
+
// out at the literal 'Untitled' — are gone, so every surface now also honors
|
|
48
|
+
// the object's `displayNameField` + type-aware derivation + `Record #<id>`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a URL-requested view name against the object's actual view ids.
|
|
10
|
+
*
|
|
11
|
+
* Canonical view ids are fully qualified (`<object>.<viewKind>`, see
|
|
12
|
+
* MetadataProvider), but nav items emit their `viewName` verbatim — usually
|
|
13
|
+
* the short form — and legacy embedded listViews carry bare keys (including
|
|
14
|
+
* the `'all'` fallback view). Accept both directions (#2217):
|
|
15
|
+
*
|
|
16
|
+
* 1. exact id match;
|
|
17
|
+
* 2. short name (no `.`) → retry as `<objectName>.<name>`;
|
|
18
|
+
* 3. qualified name → retry with the `<objectName>.` prefix stripped.
|
|
19
|
+
*
|
|
20
|
+
* Returns the matching view id, or `undefined` when nothing matches — the
|
|
21
|
+
* caller decides how to fall back (and should warn rather than swallow it).
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveViewId(requested: string | undefined | null, viewIds: readonly string[], objectName: string): string | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a URL-requested view name against the object's actual view ids.
|
|
10
|
+
*
|
|
11
|
+
* Canonical view ids are fully qualified (`<object>.<viewKind>`, see
|
|
12
|
+
* MetadataProvider), but nav items emit their `viewName` verbatim — usually
|
|
13
|
+
* the short form — and legacy embedded listViews carry bare keys (including
|
|
14
|
+
* the `'all'` fallback view). Accept both directions (#2217):
|
|
15
|
+
*
|
|
16
|
+
* 1. exact id match;
|
|
17
|
+
* 2. short name (no `.`) → retry as `<objectName>.<name>`;
|
|
18
|
+
* 3. qualified name → retry with the `<objectName>.` prefix stripped.
|
|
19
|
+
*
|
|
20
|
+
* Returns the matching view id, or `undefined` when nothing matches — the
|
|
21
|
+
* caller decides how to fall back (and should warn rather than swallow it).
|
|
22
|
+
*/
|
|
23
|
+
export function resolveViewId(requested, viewIds, objectName) {
|
|
24
|
+
if (!requested)
|
|
25
|
+
return undefined;
|
|
26
|
+
const has = (id) => viewIds.includes(id);
|
|
27
|
+
if (has(requested))
|
|
28
|
+
return requested;
|
|
29
|
+
const prefix = `${objectName}.`;
|
|
30
|
+
if (!requested.includes('.') && has(prefix + requested)) {
|
|
31
|
+
return prefix + requested;
|
|
32
|
+
}
|
|
33
|
+
if (requested.startsWith(prefix) && has(requested.slice(prefix.length))) {
|
|
34
|
+
return requested.slice(prefix.length);
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
export declare function warnSuppressedListNav(objectName: string, viewId: string, viewDef: Record<string, unknown> | null | undefined, listSchema: Record<string, unknown> | null | undefined): boolean;
|
|
9
|
+
/** Test-only: clear the one-shot warning memory. */
|
|
10
|
+
export declare function resetSuppressedListNavWarnings(): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* ADR-0053: on an object's default list ("views" mode) the ViewTabBar is the
|
|
10
|
+
* only navigation control — `userFilters` / `quickFilters` belong to page
|
|
11
|
+
* lists (InterfaceListPage, "filters" mode) and are suppressed by
|
|
12
|
+
* `ObjectView.renderListView`.
|
|
13
|
+
*
|
|
14
|
+
* The suppression is correct, but until the phase-4 guardrail (zod `refine`
|
|
15
|
+
* + `check` rule) lands, an author who puts `userFilters` on an object list
|
|
16
|
+
* view gets a valid schema and a page that renders nothing where they expect
|
|
17
|
+
* filter controls — zero signal at any layer (#2219). Surface the drop with
|
|
18
|
+
* a one-shot console warning per object/view.
|
|
19
|
+
*
|
|
20
|
+
* Returns whether the view carried a suppressed field (mainly for tests).
|
|
21
|
+
*/
|
|
22
|
+
const warned = new Set();
|
|
23
|
+
export function warnSuppressedListNav(objectName, viewId, viewDef, listSchema) {
|
|
24
|
+
const offending = ['userFilters', 'quickFilters'].filter((k) => (viewDef?.[k] ?? listSchema?.[k]) != null);
|
|
25
|
+
if (offending.length === 0)
|
|
26
|
+
return false;
|
|
27
|
+
const key = `${objectName}.${viewId}`;
|
|
28
|
+
if (!warned.has(key)) {
|
|
29
|
+
warned.add(key);
|
|
30
|
+
console.warn(`[ObjectView] View "${viewId}" on object "${objectName}" defines ` +
|
|
31
|
+
`${offending.join(' and ')}, which are ignored on an object list view ` +
|
|
32
|
+
`(ADR-0053 "views" mode — the view switcher is the only nav control here). ` +
|
|
33
|
+
`Move them to a page list (InterfaceListPage "filters" mode).`);
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/** Test-only: clear the one-shot warning memory. */
|
|
38
|
+
export function resetSuppressedListNavWarnings() {
|
|
39
|
+
warned.clear();
|
|
40
|
+
}
|
|
@@ -13,7 +13,7 @@ import { DrillNavigationProvider } from '@object-ui/react';
|
|
|
13
13
|
import { useOpenRecordList } from './useOpenRecordList';
|
|
14
14
|
import { ModalForm } from '@object-ui/plugin-form';
|
|
15
15
|
import { DashboardConfigPanel } from './DashboardConfigPanel';
|
|
16
|
-
import {
|
|
16
|
+
import { useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
17
17
|
import { toast } from 'sonner';
|
|
18
18
|
import { Empty, EmptyTitle, EmptyDescription, Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@object-ui/components';
|
|
19
19
|
import { LayoutDashboard, Pencil, TrendingUp, BarChart3, LineChart, PieChart, Table2, LayoutGrid, Plus, } from 'lucide-react';
|
|
@@ -77,8 +77,7 @@ export function DashboardView({ dataSource }) {
|
|
|
77
77
|
const { dashboardLabel, dashboardDescription } = useObjectLabel();
|
|
78
78
|
// Editing a dashboard mutates the SHARED definition, so it is an admin-only
|
|
79
79
|
// quick-edit affordance (mirrors ObjectView's view-config gate).
|
|
80
|
-
const
|
|
81
|
-
const isAdmin = user?.role === 'admin';
|
|
80
|
+
const isAdmin = useIsWorkspaceAdmin();
|
|
82
81
|
const [isLoading, setIsLoading] = useState(true);
|
|
83
82
|
const [configPanelOpen, setConfigPanelOpen] = useState(false);
|
|
84
83
|
const [selectedWidgetId, setSelectedWidgetId] = useState(null);
|
|
@@ -53,8 +53,9 @@ function resolveSourceView(objectDef, sourceView) {
|
|
|
53
53
|
/**
|
|
54
54
|
* Default column set when the resolved view carries none — mirrors
|
|
55
55
|
* ObjectView's data-mode fallback so an interface page never renders a
|
|
56
|
-
* column-less grid. Priority:
|
|
57
|
-
* business fields (system/audit columns
|
|
56
|
+
* column-less grid. Priority: the `highlightFields` semantic role
|
|
57
|
+
* (ADR-0085), else the first business fields (system/audit columns
|
|
58
|
+
* excluded).
|
|
58
59
|
*/
|
|
59
60
|
const SYSTEM_FIELDS = new Set([
|
|
60
61
|
'id', 'created_at', 'createdAt', 'updated_at', 'updatedAt',
|
|
@@ -62,8 +63,9 @@ const SYSTEM_FIELDS = new Set([
|
|
|
62
63
|
'updated_by', 'updatedBy', '_version', '_rev',
|
|
63
64
|
]);
|
|
64
65
|
function defaultColumnsFromObject(objectDef) {
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
const curated = objectDef?.highlightFields;
|
|
67
|
+
if (Array.isArray(curated) && curated.length > 0) {
|
|
68
|
+
return curated.filter((n) => objectDef.fields?.[n]);
|
|
67
69
|
}
|
|
68
70
|
const fields = objectDef?.fields;
|
|
69
71
|
if (fields && typeof fields === 'object') {
|
|
@@ -319,7 +321,10 @@ export function InterfaceListPage({ page, className, onConfigChange, reserveEdit
|
|
|
319
321
|
showGroup: false,
|
|
320
322
|
showColor: false,
|
|
321
323
|
allowExport: false,
|
|
322
|
-
|
|
324
|
+
// Inline record editing is a page-authored property: a list block opts in
|
|
325
|
+
// via `userActions.editInline` (default off). When on, clicking a cell
|
|
326
|
+
// edits it with the dedicated field widgets, same as the object views.
|
|
327
|
+
inlineEdit: userActions.editInline === true,
|
|
323
328
|
};
|
|
324
329
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
325
330
|
}, [objectDefName, viewDefJson, cfg]);
|
package/dist/views/ObjectView.js
CHANGED
|
@@ -15,7 +15,7 @@ import { parseUserFilterParams, applyUserFilterParams } from './userFilterUrlSta
|
|
|
15
15
|
const ObjectChart = lazy(() => import('@object-ui/plugin-charts').then((m) => ({ default: m.ObjectChart })));
|
|
16
16
|
const ImportWizard = lazy(() => import('@object-ui/plugin-grid').then((m) => ({ default: m.ImportWizard })));
|
|
17
17
|
import { ListView } from '@object-ui/plugin-list';
|
|
18
|
-
import { ObjectView as PluginObjectView, ViewTabBar, ManageViewsDialog } from '@object-ui/plugin-view';
|
|
18
|
+
import { ObjectView as PluginObjectView, ViewTabBar, ManageViewsDialog, deriveRecordSurface, overlayWidthFor } from '@object-ui/plugin-view';
|
|
19
19
|
// Plugin registration is handled by the host app (e.g. apps/console/src/main.tsx
|
|
20
20
|
// uses ComponentRegistry.registerLazy so heavy plugins stay code-split).
|
|
21
21
|
// Do NOT add eager `import '@object-ui/plugin-*'` side-effect imports here.
|
|
@@ -34,10 +34,12 @@ import { ManagedByBadge } from '../components/ManagedByBadge';
|
|
|
34
34
|
import { RecordDetailView } from './RecordDetailView';
|
|
35
35
|
import { resolveCrudAffordances } from '../utils/crudAffordances';
|
|
36
36
|
import { resolveManagedByEmptyState } from '../utils/managedByEmptyState';
|
|
37
|
+
import { resolveViewId } from '../utils/resolveViewId';
|
|
38
|
+
import { warnSuppressedListNav } from '../utils/warnSuppressedListNav';
|
|
37
39
|
import { useObjectActions } from '../hooks/useObjectActions';
|
|
38
40
|
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
39
41
|
import { usePermissions } from '@object-ui/permissions';
|
|
40
|
-
import { useAuth } from '@object-ui/auth';
|
|
42
|
+
import { useAuth, useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
41
43
|
import { useRealtimeSubscription, useConflictResolution } from '@object-ui/collaboration';
|
|
42
44
|
import { ActionProvider, useNavigationOverlay, SchemaRenderer } from '@object-ui/react';
|
|
43
45
|
import { toast } from 'sonner';
|
|
@@ -190,8 +192,9 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
190
192
|
'updated_by', 'updatedBy', '_version', '_rev',
|
|
191
193
|
]);
|
|
192
194
|
let defaultColumns = [];
|
|
193
|
-
|
|
194
|
-
|
|
195
|
+
const curated = objectDef?.highlightFields;
|
|
196
|
+
if (Array.isArray(curated) && curated.length > 0) {
|
|
197
|
+
defaultColumns = curated.filter((n) => objectDef.fields?.[n]);
|
|
195
198
|
}
|
|
196
199
|
else if (objectDef?.fields) {
|
|
197
200
|
defaultColumns = Object.entries(objectDef.fields)
|
|
@@ -253,7 +256,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
253
256
|
const [recordCount, setRecordCount] = useState(undefined);
|
|
254
257
|
// Admin users automatically get design tools (no toggle needed)
|
|
255
258
|
const { user, activeOrganization } = useAuth();
|
|
256
|
-
const isAdmin =
|
|
259
|
+
const isAdmin = useIsWorkspaceAdmin();
|
|
257
260
|
const { can } = usePermissions();
|
|
258
261
|
// Get Object Definition. The outer ObjectView wrapper already guards the
|
|
259
262
|
// missing-object case, so this always resolves while this component is
|
|
@@ -430,7 +433,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
430
433
|
// Resolve Views from objectDef.listViews (camelCase per @objectstack/spec)
|
|
431
434
|
const views = useMemo(() => {
|
|
432
435
|
// Default column resolution priority:
|
|
433
|
-
// 1. `
|
|
436
|
+
// 1. The `highlightFields` semantic role (ADR-0085).
|
|
434
437
|
// 2. Business fields only — exclude system-managed identifiers/audit
|
|
435
438
|
// columns (id, created_at, updated_at, …) and fields explicitly
|
|
436
439
|
// marked hidden/readonly on the schema. First 5 kept for compactness.
|
|
@@ -440,8 +443,9 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
440
443
|
'updated_by', 'updatedBy', '_version', '_rev',
|
|
441
444
|
]);
|
|
442
445
|
const resolveDefaultColumns = () => {
|
|
443
|
-
|
|
444
|
-
|
|
446
|
+
const curated = objectDef.highlightFields;
|
|
447
|
+
if (Array.isArray(curated) && curated.length > 0) {
|
|
448
|
+
return curated.filter((n) => objectDef.fields?.[n]);
|
|
445
449
|
}
|
|
446
450
|
if (objectDef.fields) {
|
|
447
451
|
return Object.entries(objectDef.fields)
|
|
@@ -606,7 +610,20 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
606
610
|
const def = views.find((v) => v.isDefault);
|
|
607
611
|
return def?.id;
|
|
608
612
|
}, [views]);
|
|
609
|
-
|
|
613
|
+
// Canonical view ids are fully qualified (`<object>.<viewKind>`, see
|
|
614
|
+
// MetadataProvider), but nav items emit `viewName` verbatim — usually
|
|
615
|
+
// the short form — and legacy embedded listViews carry bare keys (incl.
|
|
616
|
+
// the `'all'` fallback). Resolve the URL-requested name in both
|
|
617
|
+
// directions, and never swallow a miss silently (#2217).
|
|
618
|
+
const requestedViewId = viewId || searchParams.get('view') || undefined;
|
|
619
|
+
const resolvedViewId = useMemo(() => resolveViewId(requestedViewId, views.map((v) => v.id), objectDef.name), [requestedViewId, views, objectDef.name]);
|
|
620
|
+
useEffect(() => {
|
|
621
|
+
if (requestedViewId && !resolvedViewId) {
|
|
622
|
+
console.warn(`[ObjectView] Requested view "${requestedViewId}" not found on object "${objectDef.name}"; ` +
|
|
623
|
+
`falling back to the default view. Known views: ${views.map((v) => v.id).join(', ')}`);
|
|
624
|
+
}
|
|
625
|
+
}, [requestedViewId, resolvedViewId, objectDef.name, views]);
|
|
626
|
+
const activeViewId = resolvedViewId || defaultViewId || views[0]?.id;
|
|
610
627
|
const baseView = views.find((v) => v.id === activeViewId) || views[0];
|
|
611
628
|
const activeView = viewDraft && viewDraft.id === baseView?.id
|
|
612
629
|
? { ...baseView, ...viewDraft }
|
|
@@ -874,8 +891,26 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
874
891
|
// RecordDetailView owns its own route — only same-page click navigation
|
|
875
892
|
// is drawer-by-default. Per-view config can still override (e.g. a heavy
|
|
876
893
|
// detail object can set `navigation.mode = 'page'`).
|
|
877
|
-
const detailNavigation = useMemo(() =>
|
|
878
|
-
|
|
894
|
+
const detailNavigation = useMemo(() => {
|
|
895
|
+
const authored = activeView?.navigation ?? objectDef.navigation;
|
|
896
|
+
if (authored) {
|
|
897
|
+
// Authored config wins. For an overlay, resolve the `size`
|
|
898
|
+
// bucket (or 'auto') to a viewport-clamped width when no explicit
|
|
899
|
+
// width was given — #2578: a pixel width can't be authored blind.
|
|
900
|
+
if (authored.mode === 'page')
|
|
901
|
+
return authored;
|
|
902
|
+
return {
|
|
903
|
+
...authored,
|
|
904
|
+
width: authored.width ?? overlayWidthFor(authored.size, objectDef),
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
// #2578: derive surface + width from FIELD COUNT. Field-heavy → full
|
|
908
|
+
// page (cramped in a drawer); light → a drawer sized to the content
|
|
909
|
+
// and clamped to the viewport. Mobile always pages (in deriveRecordSurface).
|
|
910
|
+
return deriveRecordSurface(objectDef) === 'page'
|
|
911
|
+
? { mode: 'page' }
|
|
912
|
+
: { mode: 'drawer', width: overlayWidthFor('auto', objectDef) };
|
|
913
|
+
}, [activeView?.navigation, objectDef]);
|
|
879
914
|
const drawerRecordId = searchParams.get('recordId');
|
|
880
915
|
/**
|
|
881
916
|
* URL-derived equality filters in the form `?filter[<field>]=<value>`.
|
|
@@ -1066,6 +1101,11 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
1066
1101
|
className: 'h-[400px] w-full',
|
|
1067
1102
|
} }) }, key));
|
|
1068
1103
|
}
|
|
1104
|
+
// ADR-0053 wrong-context authoring: userFilters/quickFilters on an
|
|
1105
|
+
// object list view are suppressed below — say so instead of letting
|
|
1106
|
+
// the author stare at a toolbar with nothing where their filter
|
|
1107
|
+
// controls should be (#2219).
|
|
1108
|
+
warnSuppressedListNav(objectDef.name, viewDef.id || viewDef.name || '', viewDef, listSchema);
|
|
1069
1109
|
const fullSchema = {
|
|
1070
1110
|
...listSchema,
|
|
1071
1111
|
// Propagate appearance/view-config properties for live preview
|
|
@@ -1302,6 +1342,8 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
1302
1342
|
persistViewPatch(viewDef.id, viewDef, { filter });
|
|
1303
1343
|
}, onHiddenFieldsChange: (hidden) => {
|
|
1304
1344
|
persistViewPatch(viewDef.id, viewDef, { hiddenFields: hidden });
|
|
1345
|
+
}, onInlineEditChange: (next) => {
|
|
1346
|
+
persistViewPatch(viewDef.id, viewDef, { inlineEdit: next });
|
|
1305
1347
|
}, onColumnStateChange: (state) => {
|
|
1306
1348
|
persistViewPatch(viewDef.id, viewDef, { columnState: state });
|
|
1307
1349
|
}, userFilterSelections: initialUfSelections, onUserFilterSelectionsChange: handleUserFilterSelectionsChange, dataSource: ds }, key));
|
|
@@ -1367,11 +1409,22 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
1367
1409
|
// Import buttons stay visible without pushing the page
|
|
1368
1410
|
// title off-screen.
|
|
1369
1411
|
mobileMaxVisible: 0,
|
|
1370
|
-
} })))] }) }) }), affordances.create && can(objectDef.name, 'create') && (_jsx("button", { type: "button", onClick: actions.create, className: "sm:hidden fixed right-4 bottom-36 z-40 h-12 w-12 rounded-full bg-primary text-primary-foreground shadow-lg active:scale-95 transition-transform inline-flex items-center justify-center", "aria-label": t('console.objectView.new'), "data-testid": "mobile-fab-create", children: _jsx(Plus, { className: "h-5 w-5" }) })), showImport && (_jsx(Suspense, { fallback: null, children: _jsx(ImportWizard, { open: showImport, onOpenChange: setShowImport, objectName: objectDef.name, objectLabel: objectLabel(objectDef), fields: Object.entries(objectDef.fields || {})
|
|
1412
|
+
} })))] }) }) }), affordances.create && can(objectDef.name, 'create') && (_jsx("button", { type: "button", onClick: actions.create, className: "sm:hidden fixed right-4 bottom-36 z-40 h-12 w-12 rounded-full bg-primary text-primary-foreground shadow-lg active:scale-95 transition-transform inline-flex items-center justify-center", "aria-label": t('console.objectView.new'), "data-testid": "mobile-fab-create", children: _jsx(Plus, { className: "h-5 w-5" }) })), showImport && (_jsx(Suspense, { fallback: null, children: _jsx(ImportWizard, { open: showImport, onOpenChange: setShowImport, objectName: objectDef.name, objectLabel: objectLabel(objectDef), fields: Object.entries(objectDef.fields || {})
|
|
1413
|
+
// Only writable fields are importable targets. Computed
|
|
1414
|
+
// types (formula/summary/autonumber) and fields flagged
|
|
1415
|
+
// readonly / write:false are server-rejected, so we omit
|
|
1416
|
+
// them from the mapping step rather than let a user map to
|
|
1417
|
+
// a column the import will silently drop.
|
|
1418
|
+
.filter(([, def]) => !['formula', 'summary', 'autonumber'].includes(def?.type) &&
|
|
1419
|
+
!def?.readonly &&
|
|
1420
|
+
def?.permissions?.write !== false)
|
|
1421
|
+
.map(([name, def]) => ({
|
|
1371
1422
|
name,
|
|
1372
1423
|
label: def?.label || name,
|
|
1373
1424
|
type: def?.type || 'text',
|
|
1374
1425
|
required: !!def?.required,
|
|
1426
|
+
// Enum options seed the downloadable template's example row.
|
|
1427
|
+
...(def?.options ? { options: def.options } : {}),
|
|
1375
1428
|
})), dataSource: dataSource, onComplete: (result) => {
|
|
1376
1429
|
setRefreshKey(k => k + 1);
|
|
1377
1430
|
const ok = result.importedRows;
|
package/dist/views/PageView.js
CHANGED
|
@@ -15,7 +15,7 @@ import { SchemaRenderer, useAdapter } from '@object-ui/react';
|
|
|
15
15
|
import { Empty, EmptyTitle, EmptyDescription, Spinner } from '@object-ui/components';
|
|
16
16
|
import { FileText, Pencil } from 'lucide-react';
|
|
17
17
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
18
|
-
import {
|
|
18
|
+
import { useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
19
19
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
20
20
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
21
21
|
import { useExpressionContext } from '../providers/ExpressionProvider';
|
|
@@ -31,8 +31,7 @@ export function PageView() {
|
|
|
31
31
|
const location = useLocation();
|
|
32
32
|
// Editing a page mutates the shared metadata definition, so the entry point
|
|
33
33
|
// is admin-only (mirrors the view/report/dashboard runtime editors).
|
|
34
|
-
const
|
|
35
|
-
const isAdmin = user?.role === 'admin';
|
|
34
|
+
const isAdmin = useIsWorkspaceAdmin();
|
|
36
35
|
const { pages, objects, getTypeStatus } = useMetadata();
|
|
37
36
|
// ADR-0048 Phase 2 — prefer the page owned by the current app's package so
|
|
38
37
|
// two packages shipping `page/<same-name>` each resolve within their own
|