@object-ui/app-shell 5.0.2 → 5.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,133 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 5.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [8955b9c]
8
+ - @object-ui/components@5.1.1
9
+ - @object-ui/fields@5.1.1
10
+ - @object-ui/layout@5.1.1
11
+ - @object-ui/types@5.1.1
12
+ - @object-ui/core@5.1.1
13
+ - @object-ui/i18n@5.1.1
14
+ - @object-ui/react@5.1.1
15
+ - @object-ui/data-objectstack@5.1.1
16
+ - @object-ui/auth@5.1.1
17
+ - @object-ui/permissions@5.1.1
18
+ - @object-ui/collaboration@5.1.1
19
+ - @object-ui/providers@5.1.1
20
+
21
+ ## 5.1.0
22
+
23
+ ### Minor Changes
24
+
25
+ - d1ec6a2: Fold inline-edit into the page-header overflow menu (HubSpot/Lightning
26
+ pattern) and remove the orphan "Edit fields" toolbar row that previously
27
+ floated between the tab strip and the first detail section.
28
+ - `@object-ui/app-shell` `RecordDetailView`: injects a new `sys_inline_edit`
29
+ system action that appears in the ⋯ overflow menu and dispatches a
30
+ `objectui:record:inline-edit-toggle` window CustomEvent (filtered by
31
+ recordId + objectName).
32
+ - `@object-ui/plugin-detail` `DetailView`: listens for that event to
33
+ toggle inline-edit mode; the in-page toolbar now renders only during
34
+ active editing / save error / locked states, so the idle layout flows
35
+ tabs → first section card with no orphan row.
36
+ - `@object-ui/components` layout containers: extended `KNOWN_LABEL_DICT`
37
+ with zh-CN + zh-TW translations for common CRM related-list labels
38
+ (Quotes / Products / Contacts / Accounts / Leads / Opportunities /
39
+ Cases / Campaigns / Approvals / Documents / Emails / Calls / Meetings
40
+ / Open Tasks / Closed Tasks), so authored English labels auto-translate
41
+ in `page:accordion` / `page:tabs` items.
42
+
43
+ - cf30cc2: Polish Lightning record detail page layout.
44
+ - `record:details` sections now render with Card chrome by default when a `title` is present, restoring visual grouping that was missing on pages like the opportunity detail page.
45
+ - Section labels can be translated via the `{ns}.objects.{objectName}._sections.{name}.label` convention. Author each section with a stable `name` (e.g. `info`, `forecast`) and the renderer picks up the locale-specific label automatically. Falls back to the literal `label` when no translation exists.
46
+ - The `page:header` action toolbar now collapses into a `⋯` overflow menu when more than two actions are present. The first business action stays inline; secondary system actions (Edit / Share / Delete) move into the menu, with destructive styling applied to Delete.
47
+ - Header action labels resolve via the `{ns}.objects.{objectName}._actions.{name}.label` convention.
48
+ - Removed the meaningless field-count Badge from collapsible section headers (the `2` chip next to "Description"). Field-count metadata wasn't useful in the header and added visual noise.
49
+ - Synth-path `sys_delete` now carries `variant: 'destructive'` so the overflow menu can color it appropriately.
50
+
51
+ - c0b236f: Platform detail/form polish:
52
+ - **Auto-section grouping**: When an object has no authored `views.form.sections`, the detail page now splits fields into a primary section and a collapsible "More details" section based on a field-type/name heuristic (textarea / markdown / description / notes / remarks). Eliminates the wall-of-fields layout on objects without explicit detail metadata.
53
+ - **FormSection card chrome**: `FormSection` now accepts `showBorder`. Defaults to `true` for titled sections (Card wrapper) and `false` for untitled sections (flat). Same auto-default already applied to `DetailSection`.
54
+ - **Origin breadcrumb**: Navigating from a list/kanban into a record now records the source view; the detail page shows a `← <view label>` back-link above the page header.
55
+ - New i18n key `detail.sectionMoreDetails` (en + zh-CN).
56
+
57
+ ### Patch Changes
58
+
59
+ - d51a577: feat(platform): Discussion attachments + @mention directory + Reference Rail aside
60
+ - **Discussion attachments** — `RichTextCommentInput` now accepts an `extraSlot`
61
+ and a `canSubmitEmpty` flag so hosts can mount the existing
62
+ `CommentAttachment` composer beneath the editor without forking the toolbar.
63
+ `RecordActivityTimeline` plumbs the attachments through
64
+ `DiscussionContext.onUploadAttachments` and submits attachment-only comments.
65
+ - **@mention directory** — `DiscussionContext` gains a `mentionSuggestions`
66
+ field; `RecordDetailView` populates it from the host `sys_user` collection so
67
+ `@` autocomplete in the composer now resolves against real users.
68
+ - **Reference Rail** — New `record:reference_rail` renderer + a dedicated
69
+ `aside` region emitted by `buildDefaultPageSchema` whenever a record has
70
+ ≥ 2 related lists. The rail surfaces a Salesforce/HubSpot-style snapshot
71
+ of related collections (count badge + top 3 records) on `xl+` viewports.
72
+ - **Layout** — `PageRenderer`'s structured-layout `<aside>` wrappers now honor
73
+ `aside.className`, letting schemas attach responsive utilities like
74
+ `hidden xl:flex` to the rail region.
75
+
76
+ - 1976691: Fix the drawer "Open as full page" (maximize) button on the record drawer
77
+ which threw `TypeError: name.indexOf is not a function` and prevented
78
+ navigation to the dedicated detail page.
79
+ - `@object-ui/app-shell` `ObjectView`: pass `objectDef.name` (string) — not
80
+ the whole `objectDef` — into `viewLabel(...)` when computing the
81
+ `originState.from.label` for both drawer-navigate and list-navigate
82
+ flows. Two call sites fixed.
83
+ - `@object-ui/i18n` `useObjectLabel`: harden `stripNamespace` so it
84
+ tolerates non-string inputs and returns an empty string instead of
85
+ throwing, providing a safety net for similar future regressions.
86
+
87
+ - a49f300: feat(detail): per-object Reference Rail opt-out via `objectDef.detail.hideReferenceRail`
88
+
89
+ The Record-detail Reference Rail (right-hand related-list summary cards)
90
+ can now be suppressed on a per-object basis without authoring a full
91
+ custom `Page`. Catalog-style objects (Product, Task) ship with the rail
92
+ off by default; hub objects (Account, Opportunity, Contact, Case) keep it
93
+ on.
94
+ - `RecordDetailView` now reads `(objectDef as any)?.detail?.hideReferenceRail`
95
+ and `…?.hideRelatedTab` and threads them to `buildDefaultPageSchema`.
96
+ - The Reference Rail renderer also accepts entries authored as either a
97
+ flat `entries` array or nested under `properties.entries`, so explicit
98
+ `Page` authors can opt-in via the standard spec shape.
99
+ - See `packages/plugin-detail/README.md` (Reference Rail decision matrix)
100
+ for the rationale and per-object guidance.
101
+
102
+ - e9767b0: Remove dead `sys_presence` REST probes from `RecordDetailView` and `AppHeader`. Real-time
103
+ presence does not belong in a regular REST collection — the feature is being redesigned
104
+ behind a transport-level `<PresenceProvider>` (see ROADMAP). This change removes the
105
+ probe (and associated state / unused UI mounts) so the browser no longer makes silently
106
+ swallowed 404 requests on every record open / app navigation. UI surface area is
107
+ unchanged for end users (the previous code never rendered viewers when the probe failed).
108
+ - Updated dependencies [bd8447d]
109
+ - Updated dependencies [fbd5052]
110
+ - Updated dependencies [d51a577]
111
+ - Updated dependencies [1976691]
112
+ - Updated dependencies [d1ec6a2]
113
+ - Updated dependencies [cf30cc2]
114
+ - Updated dependencies [5b80cfd]
115
+ - Updated dependencies [49b1760]
116
+ - Updated dependencies [c0b236f]
117
+ - Updated dependencies [d548d6b]
118
+ - @object-ui/components@5.1.0
119
+ - @object-ui/react@5.1.0
120
+ - @object-ui/i18n@5.1.0
121
+ - @object-ui/types@5.1.0
122
+ - @object-ui/core@5.1.0
123
+ - @object-ui/data-objectstack@5.1.0
124
+ - @object-ui/fields@5.1.0
125
+ - @object-ui/layout@5.1.0
126
+ - @object-ui/auth@5.1.0
127
+ - @object-ui/collaboration@5.1.0
128
+ - @object-ui/permissions@5.1.0
129
+ - @object-ui/providers@5.1.0
130
+
3
131
  ## 5.0.2
4
132
 
5
133
  ### Patch Changes
@@ -58,14 +58,12 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
58
58
  const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
59
59
  const { currentAppName, recordTitle } = useNavigationContext();
60
60
  const mobileSwitcher = useMobileViewSwitcher();
61
- const [apiPresenceUsers, setApiPresenceUsers] = useState(null);
62
61
  const [apiActivities, setApiActivities] = useState(null);
63
62
  /** M10.8: in-header notifications. Polled from sys_notification scoped to current user. */
64
63
  const [notifications, setNotifications] = useState([]);
65
64
  // Once the server returns 404 for these collections we stop retrying for
66
65
  // the lifetime of the page — they're optional features and re-requesting
67
66
  // on every navigation creates console noise + wasted round trips.
68
- const presenceUnavailableRef = useRef(false);
69
67
  const activityUnavailableRef = useRef(false);
70
68
  const notificationsUnavailableRef = useRef(false);
71
69
  /** M11.C15: pending approvals count for the topbar shortcut. */
@@ -79,29 +77,21 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
79
77
  // registered on the server. Either signal means the feature is
80
78
  // unavailable — disable it for the rest of the page.
81
79
  const isMissingResource = (err) => err?.httpStatus === 404 || err?.status === 404 || err?.code === 'object_not_found';
82
- const presenceP = presenceUnavailableRef.current
83
- ? Promise.resolve({ data: [] })
84
- : dataSource.find('sys_presence').catch((err) => {
85
- if (isMissingResource(err))
86
- presenceUnavailableRef.current = true;
87
- return { data: [] };
88
- });
89
- const activityP = activityUnavailableRef.current
90
- ? Promise.resolve({ data: [] })
91
- : dataSource
80
+ // Tenant-wide presence ("who else is online?") is intentionally NOT
81
+ // probed here. Presence is real-time ephemeral state that does not
82
+ // belong in a regular REST collection. The feature is staged behind a
83
+ // transport-level provider (<PresenceProvider>) which is not yet
84
+ // wired — see ROADMAP for the realtime plan.
85
+ if (activityUnavailableRef.current)
86
+ return;
87
+ try {
88
+ const activityResult = await dataSource
92
89
  .find('sys_activity', { $orderby: { timestamp: 'desc' }, $top: 20 })
93
90
  .catch((err) => {
94
91
  if (isMissingResource(err))
95
92
  activityUnavailableRef.current = true;
96
93
  return { data: [] };
97
94
  });
98
- try {
99
- const [presenceResult, activityResult] = await Promise.all([presenceP, activityP]);
100
- if (presenceResult.data?.length) {
101
- const users = presenceResult.data.filter((u) => typeof u.userId === 'string');
102
- if (users.length)
103
- setApiPresenceUsers(users);
104
- }
105
95
  if (activityResult.data?.length) {
106
96
  const items = activityResult.data.filter((a) => typeof a.type === 'string');
107
97
  if (items.length)
@@ -277,7 +267,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
277
267
  const now = new Date().toISOString();
278
268
  await Promise.all(unread.map(n => dataSource.update('sys_notification', n.id, { is_read: true, read_at: now }).catch(() => { })));
279
269
  }, [dataSource, notifications]);
280
- const activeUsers = presenceUsers ?? apiPresenceUsers ?? EMPTY_PRESENCE_USERS;
270
+ const activeUsers = presenceUsers ?? EMPTY_PRESENCE_USERS;
281
271
  const activeActivities = activities ?? apiActivities ?? [];
282
272
  const orgList = organizations ?? [];
283
273
  const hasOrgSection = isOrganizationsLoading || orgList.length > 0 || !!activeOrganization;
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
10
10
  * - ListView delegation for non-grid view types (kanban, calendar, chart, etc.)
11
11
  */
12
12
  import { useMemo, useState, useCallback, useEffect, useRef, lazy, Suspense } from 'react';
13
- import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
13
+ import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
14
14
  const ObjectChart = lazy(() => import('@object-ui/plugin-charts').then((m) => ({ default: m.ObjectChart })));
15
15
  const ImportWizard = lazy(() => import('@object-ui/plugin-grid').then((m) => ({ default: m.ImportWizard })));
16
16
  import { ListView } from '@object-ui/plugin-list';
@@ -197,6 +197,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
197
197
  const navigate = useNavigate();
198
198
  const { appName, objectName, viewId } = useParams();
199
199
  const [searchParams, setSearchParams] = useSearchParams();
200
+ const location = useLocation();
200
201
  const { showDebug } = useMetadataInspector();
201
202
  const { t } = useObjectTranslation();
202
203
  const { objectLabel, objectDescription: objectDesc, viewLabel, viewEmptyState, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
@@ -1239,13 +1240,19 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
1239
1240
  // forwarded from `navigation.view` (e.g. 'detail_form'). The view
1240
1241
  // variant is resolved by RecordDetailView from its own config, so
1241
1242
  // any non-`new_window` action lands on the record detail route.
1243
+ const originState = {
1244
+ from: {
1245
+ pathname: location.pathname + (location.search || ''),
1246
+ label: viewLabel(objectDef.name, activeView?.name ?? '', activeView?.label ?? '') || objectLabel(objectDef),
1247
+ },
1248
+ };
1242
1249
  if (viewId) {
1243
- navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path' });
1250
+ navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path', state: originState });
1244
1251
  }
1245
1252
  else {
1246
- navigate(`record/${encodeURIComponent(String(recordId))}`);
1253
+ navigate(`record/${encodeURIComponent(String(recordId))}`, { state: originState });
1247
1254
  }
1248
- }, [navigate, viewId]);
1255
+ }, [navigate, viewId, location.pathname, location.search, objectDef, activeView?.name, activeView?.label, viewLabel, objectLabel]);
1249
1256
  const navOverlay = useNavigationOverlay({
1250
1257
  navigation: detailNavigation,
1251
1258
  objectName: objectDef.name,
@@ -1566,15 +1573,21 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
1566
1573
  onEdit?.({ id: recordId });
1567
1574
  }
1568
1575
  else if (mode === 'view') {
1576
+ const originState = {
1577
+ from: {
1578
+ pathname: location.pathname + (location.search || ''),
1579
+ label: viewLabel(objectDef.name, activeView?.name ?? '', activeView?.label ?? '') || objectLabel(objectDef),
1580
+ },
1581
+ };
1569
1582
  if (viewId) {
1570
- navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path' });
1583
+ navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path', state: originState });
1571
1584
  }
1572
1585
  else {
1573
- navigate(`record/${encodeURIComponent(String(recordId))}`);
1586
+ navigate(`record/${encodeURIComponent(String(recordId))}`, { state: originState });
1574
1587
  }
1575
1588
  }
1576
1589
  },
1577
- }), [objectDef.name, onEdit, activeView?.showSearch, activeView?.showFilters, activeView?.showSort, navigate, viewId, isAdmin]);
1590
+ }), [objectDef, onEdit, activeView?.showSearch, activeView?.showFilters, activeView?.showSort, activeView?.name, activeView?.label, navigate, viewId, isAdmin, location.pathname, location.search, viewLabel, objectLabel]);
1578
1591
  return (_jsxs(ActionProvider, { context: {
1579
1592
  objectName: objectDef.name,
1580
1593
  user: currentUser,
@@ -7,15 +7,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  * the object field definitions.
8
8
  */
9
9
  import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
10
- import { useParams, useNavigate } from 'react-router-dom';
10
+ import { useParams, useNavigate, useLocation, Link } from 'react-router-dom';
11
11
  import { DetailView, RecordChatterPanel, buildDefaultPageSchema } from '@object-ui/plugin-detail';
12
12
  import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
13
- import { PresenceAvatars } from '@object-ui/collaboration';
14
13
  import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
15
14
  import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider } from '@object-ui/react';
16
15
  import { buildExpandFields } from '@object-ui/core';
17
16
  import { toast } from 'sonner';
18
- import { Database, Users } from 'lucide-react';
17
+ import { Database, ChevronLeft } from 'lucide-react';
19
18
  import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
20
19
  import { SkeletonDetail } from '../skeletons';
21
20
  import { ManagedByBadge } from '../components/ManagedByBadge';
@@ -50,6 +49,24 @@ const AUDIT_FIELD_NAMES = new Set(['created_at', 'created_by', 'updated_at', 'up
50
49
  const HIDDEN_SYSTEM_FIELD_NAMES = new Set([
51
50
  'organization_id', 'tenant_id', 'is_deleted', 'deleted_at',
52
51
  ]);
52
+ /**
53
+ * Field-type signals that suggest a "secondary / system / metadata"
54
+ * placement when auto-grouping fields. These move out of the main
55
+ * section and into a collapsible "More details" section by default,
56
+ * keeping the primary section dense with business-critical fields.
57
+ *
58
+ * The heuristic is conservative: when no objectDef metadata is available
59
+ * we surface most fields in the main section; long-form text and
60
+ * audit-by-name fields drop down.
61
+ */
62
+ const SECONDARY_FIELD_NAME_HINTS = ['description', 'notes', 'note', 'remark', 'remarks', 'comments'];
63
+ const SECONDARY_FIELD_TYPES = new Set(['textarea', 'markdown', 'html', 'rich-text', 'json', 'code']);
64
+ function isSecondaryField(fieldName, fieldDef) {
65
+ if (SECONDARY_FIELD_TYPES.has(fieldDef?.type))
66
+ return true;
67
+ const lc = fieldName.toLowerCase();
68
+ return SECONDARY_FIELD_NAME_HINTS.some((hint) => lc === hint || lc.endsWith(`_${hint}`));
69
+ }
53
70
  export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverride, recordIdOverride, embedded }) {
54
71
  const params = useParams();
55
72
  const appName = params.appName;
@@ -58,13 +75,15 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
58
75
  const { showDebug } = useMetadataInspector();
59
76
  const { user } = useAuth();
60
77
  const navigate = useNavigate();
78
+ const location = useLocation();
79
+ const originFrom = location.state?.from;
61
80
  const { t } = useObjectTranslation();
62
81
  const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
63
82
  const { isFavorite, toggleFavorite, refreshLabel: refreshFavoriteLabel } = useFavorites();
64
83
  const { addRecentItem } = useRecentItems();
65
84
  const [isLoading, setIsLoading] = useState(true);
66
85
  const [feedItems, setFeedItems] = useState([]);
67
- const [recordViewers, setRecordViewers] = useState([]);
86
+ const [mentionSuggestions, setMentionSuggestions] = useState([]);
68
87
  const [actionRefreshKey, setActionRefreshKey] = useState(0);
69
88
  const [childRelatedData, setChildRelatedData] = useState({});
70
89
  const [historyEntries, setHistoryEntries] = useState(null);
@@ -553,20 +572,56 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
553
572
  });
554
573
  return () => { cancelled = true; };
555
574
  }, [dataSource, pureRecordId, objectDef, historyEnabled]);
575
+ // Fetch a directory of active users once per dataSource mount and expose
576
+ // them as @-mention suggestions to the DiscussionContext. Capped at 50 to
577
+ // keep the dropdown tight; hosts can swap in a paginated/server-search
578
+ // implementation later by mounting their own DiscussionContextProvider.
579
+ useEffect(() => {
580
+ if (!dataSource)
581
+ return;
582
+ let cancelled = false;
583
+ (async () => {
584
+ try {
585
+ const res = await dataSource.find('sys_user', {
586
+ $top: 50,
587
+ $select: ['id', 'name', 'email', 'image'],
588
+ });
589
+ if (cancelled)
590
+ return;
591
+ const rows = Array.isArray(res) ? res : res?.data || [];
592
+ const suggestions = rows
593
+ .map((u) => ({
594
+ id: String(u.id),
595
+ label: u.name || u.email || String(u.id),
596
+ avatarUrl: u.image || undefined,
597
+ }))
598
+ .filter((s) => s.label);
599
+ setMentionSuggestions(suggestions);
600
+ }
601
+ catch {
602
+ // Silently fall back to free-text @mention when the user dir is
603
+ // unavailable (e.g. the backend has no sys_user collection).
604
+ }
605
+ })();
606
+ return () => { cancelled = true; };
607
+ }, [dataSource]);
556
608
  // Memoize so the object identity is stable across renders — otherwise
557
609
  // any effect that depends on it (e.g. the feed loader below) would
558
610
  // re-fire every render and create an infinite request loop.
559
611
  const currentUser = useMemo(() => (user ? { id: user.id, name: user.name, avatar: user.image } : FALLBACK_USER), [user?.id, user?.name, user?.image]);
560
- // Fetch presence and comments from API
612
+ // Fetch comments from API.
613
+ //
614
+ // NOTE: Record-level presence ("who else is viewing this record") used to
615
+ // be probed here by `dataSource.find('sys_presence', …)`, but that was an
616
+ // architectural mistake: presence is real-time ephemeral state and does
617
+ // not belong in a regular REST collection. The probe has been removed
618
+ // pending a proper transport-level design (WebSocket-backed
619
+ // `<PresenceProvider>` in @object-ui/collaboration). See ROADMAP for the
620
+ // realtime / OCC plan.
561
621
  useEffect(() => {
562
622
  if (!dataSource || !objectName || !pureRecordId)
563
623
  return;
564
624
  const threadId = `${objectName}:${pureRecordId}`;
565
- // Fetch record viewers
566
- dataSource.find('sys_presence', { $filter: { recordId: pureRecordId } })
567
- .then((res) => { if (res.data?.length)
568
- setRecordViewers(res.data); })
569
- .catch(() => { });
570
625
  // M10.10: Fetch persisted comments from sys_comment. Field names
571
626
  // are snake_case to match the platform-objects schema
572
627
  // (`packages/platform-objects/src/audit/sys-comment.object.ts`):
@@ -867,29 +922,55 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
867
922
  };
868
923
  }),
869
924
  }))
870
- : [
871
- {
872
- // Intentionally untitled: when there's only one auto-generated
873
- // section, DetailSection flattens it (no Card chrome, no
874
- // redundant "Details" heading).
875
- showBorder: false,
876
- fields: Object.keys(objectDef.fields || {})
877
- .filter(key => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden)
878
- .map(key => {
879
- const fieldDef = objectDef.fields[key];
880
- const refTarget = fieldDef.reference_to || fieldDef.reference;
881
- return {
882
- name: key,
883
- label: fieldDef.label || key,
884
- type: fieldDef.type || 'text',
885
- ...(fieldDef.options && { options: fieldDef.options }),
886
- ...(refTarget && { reference_to: refTarget }),
887
- ...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
888
- ...(fieldDef.currency && { currency: fieldDef.currency }),
889
- };
890
- }),
891
- },
892
- ];
925
+ : (() => {
926
+ // Auto-grouping (platform B): when no form sections are authored,
927
+ // split fields into a primary section and a collapsible
928
+ // "More details" section so long-form/secondary fields don't
929
+ // dilute the main grid. The primary section stays untitled so
930
+ // DetailSection still flattens its chrome when alone.
931
+ const allFields = Object.keys(objectDef.fields || {})
932
+ .filter((key) => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden);
933
+ const toField = (key) => {
934
+ const fieldDef = objectDef.fields[key];
935
+ const refTarget = fieldDef.reference_to || fieldDef.reference;
936
+ return {
937
+ name: key,
938
+ label: fieldDef.label || key,
939
+ type: fieldDef.type || 'text',
940
+ ...(fieldDef.options && { options: fieldDef.options }),
941
+ ...(refTarget && { reference_to: refTarget }),
942
+ ...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
943
+ ...(fieldDef.currency && { currency: fieldDef.currency }),
944
+ };
945
+ };
946
+ const primaryKeys = allFields.filter((k) => !isSecondaryField(k, objectDef.fields[k]));
947
+ const secondaryKeys = allFields.filter((k) => isSecondaryField(k, objectDef.fields[k]));
948
+ // Below ~6 primary fields the second section often looks awkward
949
+ // — keep the legacy single-untitled-section behaviour. Also
950
+ // honour the "no secondary fields" case the same way.
951
+ if (secondaryKeys.length === 0 || primaryKeys.length === 0) {
952
+ return [
953
+ {
954
+ showBorder: false,
955
+ fields: allFields.map(toField),
956
+ },
957
+ ];
958
+ }
959
+ return [
960
+ {
961
+ showBorder: false,
962
+ fields: primaryKeys.map(toField),
963
+ },
964
+ {
965
+ name: 'details',
966
+ title: sectionLabel(objectDef.name, 'details', t('detail.sectionMoreDetails', 'More details')),
967
+ collapsible: true,
968
+ defaultCollapsed: false,
969
+ showBorder: true,
970
+ fields: secondaryKeys.map(toField),
971
+ },
972
+ ];
973
+ })();
893
974
  // Audit fields (created_at/created_by/updated_at/updated_by) are NOT
894
975
  // appended as a section here — they are surfaced by `<RecordMetaFooter>`
895
976
  // (rendered by DetailView) as a single subtle line below the content,
@@ -1143,6 +1224,24 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1143
1224
  const affordances = resolveCrudAffordances(objectDef);
1144
1225
  const items = [];
1145
1226
  if (affordances.edit) {
1227
+ // Inline-edit toggle. Surfaced ABOVE `sys_edit` so the
1228
+ // overflow menu lists field-level editing first — Lightning /
1229
+ // HubSpot put inline edit ("Edit details") above the modal /
1230
+ // form-page edit because in-page editing is the higher-frequency
1231
+ // interaction. Communicates with DetailView via a window event
1232
+ // so we don't need to lift inline-edit state out of the plugin.
1233
+ items.push({
1234
+ name: 'sys_inline_edit',
1235
+ label: t('detail.editFieldsInline', { defaultValue: 'Edit fields' }),
1236
+ type: 'script',
1237
+ locations: ['record_header'],
1238
+ variant: 'outline',
1239
+ onClick: () => {
1240
+ window.dispatchEvent(new CustomEvent('objectui:record:inline-edit-toggle', {
1241
+ detail: { recordId: pureRecordId, objectName },
1242
+ }));
1243
+ },
1244
+ });
1146
1245
  items.push({
1147
1246
  name: 'sys_edit',
1148
1247
  label: t('detail.edit', { defaultValue: 'Edit' }),
@@ -1191,7 +1290,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1191
1290
  label: t('detail.delete', { defaultValue: 'Delete' }),
1192
1291
  type: 'script',
1193
1292
  locations: ['record_header'],
1194
- variant: 'outline',
1293
+ variant: 'destructive',
1195
1294
  onClick: async () => {
1196
1295
  const msg = t('detail.deleteConfirmation', {
1197
1296
  defaultValue: 'Are you sure you want to delete this record?',
@@ -1250,9 +1349,14 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1250
1349
  headerActions: synthHeaderActions,
1251
1350
  related: synthRelated,
1252
1351
  history: synthHistory,
1352
+ // Per-object opt-outs read from `objectDef.detail.*`. Lets
1353
+ // catalog/atomic objects (product, task, ...) keep a focused
1354
+ // single-column layout instead of inheriting the rail.
1355
+ hideReferenceRail: objectDef?.detail?.hideReferenceRail === true || undefined,
1356
+ hideRelatedTab: objectDef?.detail?.hideRelatedTab === true || undefined,
1253
1357
  ...(assignedSlots ? { slots: assignedSlots } : {}),
1254
1358
  });
1255
- return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [_jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, headerSystemActions: synthSystemActions, isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, children: _jsx(HighlightFieldsProvider, { children: _jsx(DiscussionContextProvider, { items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, children: _jsx(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsxs("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: [_jsx(SchemaRenderer, { schema: renderedPage }), showAutoDiscussion && (_jsx("div", { className: "mt-6", children: _jsx(RecordChatterPanel, { config: {
1359
+ return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsx("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }) }), _jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, headerSystemActions: synthSystemActions, isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, children: _jsx(HighlightFieldsProvider, { children: _jsx(DiscussionContextProvider, { items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, mentionSuggestions: mentionSuggestions, children: _jsx(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsxs("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: [originFrom?.pathname && originFrom?.label && (_jsxs(Link, { to: originFrom.pathname, className: "inline-flex items-center gap-1 mb-3 text-sm text-muted-foreground hover:text-foreground transition-colors", children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), _jsx("span", { children: originFrom.label })] })), _jsx(SchemaRenderer, { schema: renderedPage }), showAutoDiscussion && (_jsx("div", { className: "mt-6", children: _jsx(RecordChatterPanel, { config: {
1256
1360
  position: 'bottom',
1257
1361
  collapsible: false,
1258
1362
  feed: {
@@ -1260,7 +1364,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1260
1364
  enableThreading: true,
1261
1365
  showCommentInput: true,
1262
1366
  },
1263
- }, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) }))] }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Page Schema', data: renderedPage }] })] }) }) }) }) }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
1367
+ }, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, mentionSuggestions: mentionSuggestions }) }))] }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Page Schema', data: renderedPage }] })] }) }) }) }) }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
1264
1368
  if (!open)
1265
1369
  setConfirmState(s => ({ ...s, open: false }));
1266
1370
  } }), _jsx(ActionParamDialog, { state: paramState, onOpenChange: (open) => {
@@ -1268,7 +1372,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1268
1372
  setParamState(s => ({ ...s, open: false }));
1269
1373
  } })] }));
1270
1374
  }
1271
- return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [_jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsx("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: _jsx(ActionProvider, { context: { record: {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, onDataLoaded: (record) => {
1375
+ return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsx("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }) }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsx("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: _jsx(ActionProvider, { context: { record: {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, onDataLoaded: (record) => {
1272
1376
  if (!record || typeof record !== 'object')
1273
1377
  return;
1274
1378
  // Resolve the same way DetailView's header does, so the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "5.0.2",
3
+ "version": "5.1.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
@@ -27,35 +27,35 @@
27
27
  "dependencies": {
28
28
  "lucide-react": "^1.16.0",
29
29
  "sonner": "^2.0.7",
30
- "@object-ui/auth": "5.0.2",
31
- "@object-ui/collaboration": "5.0.2",
32
- "@object-ui/components": "5.0.2",
33
- "@object-ui/core": "5.0.2",
34
- "@object-ui/data-objectstack": "5.0.2",
35
- "@object-ui/fields": "5.0.2",
36
- "@object-ui/i18n": "5.0.2",
37
- "@object-ui/layout": "5.0.2",
38
- "@object-ui/permissions": "5.0.2",
39
- "@object-ui/providers": "5.0.2",
40
- "@object-ui/react": "5.0.2",
41
- "@object-ui/types": "5.0.2"
30
+ "@object-ui/auth": "5.1.1",
31
+ "@object-ui/collaboration": "5.1.1",
32
+ "@object-ui/components": "5.1.1",
33
+ "@object-ui/core": "5.1.1",
34
+ "@object-ui/data-objectstack": "5.1.1",
35
+ "@object-ui/fields": "5.1.1",
36
+ "@object-ui/i18n": "5.1.1",
37
+ "@object-ui/layout": "5.1.1",
38
+ "@object-ui/permissions": "5.1.1",
39
+ "@object-ui/providers": "5.1.1",
40
+ "@object-ui/react": "5.1.1",
41
+ "@object-ui/types": "5.1.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": "^18.0.0 || ^19.0.0",
45
45
  "react-dom": "^18.0.0 || ^19.0.0",
46
46
  "react-router-dom": "^6.0.0 || ^7.0.0",
47
- "@object-ui/plugin-calendar": "^5.0.2",
48
- "@object-ui/plugin-charts": "^5.0.2",
49
- "@object-ui/plugin-chatbot": "^5.0.2",
50
- "@object-ui/plugin-dashboard": "^5.0.2",
51
- "@object-ui/plugin-designer": "^5.0.2",
52
- "@object-ui/plugin-detail": "^5.0.2",
53
- "@object-ui/plugin-form": "^5.0.2",
54
- "@object-ui/plugin-grid": "^5.0.2",
55
- "@object-ui/plugin-kanban": "^5.0.2",
56
- "@object-ui/plugin-list": "^5.0.2",
57
- "@object-ui/plugin-report": "^5.0.2",
58
- "@object-ui/plugin-view": "^5.0.2"
47
+ "@object-ui/plugin-calendar": "^5.1.1",
48
+ "@object-ui/plugin-charts": "^5.1.1",
49
+ "@object-ui/plugin-chatbot": "^5.1.1",
50
+ "@object-ui/plugin-dashboard": "^5.1.1",
51
+ "@object-ui/plugin-designer": "^5.1.1",
52
+ "@object-ui/plugin-detail": "^5.1.1",
53
+ "@object-ui/plugin-form": "^5.1.1",
54
+ "@object-ui/plugin-grid": "^5.1.1",
55
+ "@object-ui/plugin-kanban": "^5.1.1",
56
+ "@object-ui/plugin-list": "^5.1.1",
57
+ "@object-ui/plugin-report": "^5.1.1",
58
+ "@object-ui/plugin-view": "^5.1.1"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/node": "^25.9.0",