@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +562 -0
  2. package/README.md +23 -0
  3. package/dist/console/ConsoleShell.js +17 -2
  4. package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
  5. package/dist/console/home/CloudOnboardingNext.js +14 -4
  6. package/dist/console/home/HomePage.js +34 -7
  7. package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
  8. package/dist/console/organizations/OrganizationsPage.js +16 -7
  9. package/dist/hooks/useConsoleActionRuntime.js +32 -3
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +6 -0
  12. package/dist/preview/DraftChangesPanel.d.ts +3 -1
  13. package/dist/preview/DraftChangesPanel.js +6 -5
  14. package/dist/utils/deriveRelatedLists.d.ts +20 -5
  15. package/dist/utils/deriveRelatedLists.js +31 -13
  16. package/dist/utils/index.d.ts +2 -24
  17. package/dist/utils/index.js +14 -101
  18. package/dist/utils/resolveViewId.d.ts +23 -0
  19. package/dist/utils/resolveViewId.js +37 -0
  20. package/dist/utils/warnSuppressedListNav.d.ts +10 -0
  21. package/dist/utils/warnSuppressedListNav.js +40 -0
  22. package/dist/views/DashboardView.js +2 -3
  23. package/dist/views/InterfaceListPage.js +10 -5
  24. package/dist/views/ObjectView.js +65 -12
  25. package/dist/views/PageView.js +2 -3
  26. package/dist/views/RecordDetailView.js +131 -104
  27. package/dist/views/RecordFormPage.js +7 -1
  28. package/dist/views/RelatedRecordActionsBridge.d.ts +24 -0
  29. package/dist/views/RelatedRecordActionsBridge.js +114 -0
  30. package/dist/views/ReportView.js +2 -3
  31. package/dist/views/metadata-admin/PackagesPage.js +18 -7
  32. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
  33. package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
  34. package/dist/views/metadata-admin/clientValidation.js +8 -2
  35. package/dist/views/metadata-admin/color-variant-field.d.ts +1 -12
  36. package/dist/views/metadata-admin/color-variant-field.js +11 -0
  37. package/dist/views/metadata-admin/i18n.d.ts +12 -21
  38. package/dist/views/metadata-admin/i18n.js +343 -2
  39. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +25 -11
  40. package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
  41. package/dist/views/metadata-admin/permission-slice.js +70 -0
  42. package/dist/views/metadata-admin/previews/AppNavCanvas.js +11 -7
  43. package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
  44. package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
  45. package/dist/views/metadata-admin/previews/OutlineStrip.d.ts +1 -13
  46. package/dist/views/metadata-admin/previews/OutlineStrip.js +12 -0
  47. package/dist/views/metadata-admin/previews/PagePreview.js +9 -0
  48. package/dist/views/metadata-admin/previews/SourcePageEditor.d.ts +28 -0
  49. package/dist/views/metadata-admin/previews/SourcePageEditor.js +83 -0
  50. package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
  51. package/dist/views/studio-design/BuilderLanding.js +133 -0
  52. package/dist/views/studio-design/ObjectFormDesigner.d.ts +31 -0
  53. package/dist/views/studio-design/ObjectFormDesigner.js +226 -0
  54. package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
  55. package/dist/views/studio-design/ObjectSettingsPanel.js +45 -0
  56. package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
  57. package/dist/views/studio-design/ObjectValidationsPanel.js +78 -0
  58. package/dist/views/studio-design/StudioDesignSurface.d.ts +20 -0
  59. package/dist/views/studio-design/StudioDesignSurface.js +1306 -0
  60. package/dist/views/studio-design/metadataError.d.ts +23 -0
  61. package/dist/views/studio-design/metadataError.js +44 -0
  62. package/dist/views/studio-design/packages-io.d.ts +27 -0
  63. package/dist/views/studio-design/packages-io.js +61 -0
  64. package/package.json +46 -43
@@ -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
- // Sentinel used to mark empty-placeholder positions inside formatRecordTitle
36
- // so adjacent separators can be stripped in a second pass.
37
- const EMPTY_TOKEN = '\u0000';
38
- // Separator characters commonly placed between {fields} in titleFormat patterns
39
- // (hyphen, em/en dashes, pipes, slashes, middle dot, comma, colon).
40
- const SEPARATOR_CLASS = '[-\\u2013\\u2014|/·,:]';
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 { useAuth } from '@object-ui/auth';
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 { user } = useAuth();
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: curated `compactLayout`, else the first
57
- * business fields (system/audit columns excluded).
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
- if (Array.isArray(objectDef?.compactLayout) && objectDef.compactLayout.length > 0) {
66
- return objectDef.compactLayout.filter((n) => objectDef.fields?.[n]);
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
- inlineEdit: false,
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]);
@@ -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
- if (Array.isArray(objectDef?.compactLayout) && objectDef.compactLayout.length > 0) {
194
- defaultColumns = objectDef.compactLayout.filter((n) => objectDef.fields?.[n]);
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 = user?.role === 'admin';
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. `compactLayout` (curated primary business fields).
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
- if (Array.isArray(objectDef.compactLayout) && objectDef.compactLayout.length > 0) {
444
- return objectDef.compactLayout.filter((n) => objectDef.fields?.[n]);
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
- const activeViewId = viewId || searchParams.get('view') || defaultViewId || views[0]?.id;
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(() => activeView?.navigation ??
878
- objectDef.navigation ?? { mode: 'drawer', width: 'min(92vw, 1280px)' }, [activeView?.navigation, objectDef.navigation]);
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 || {}).map(([name, def]) => ({
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;
@@ -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 { useAuth } from '@object-ui/auth';
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 { user } = useAuth();
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