@object-ui/app-shell 5.0.1 → 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,174 @@
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
+
131
+ ## 5.0.2
132
+
133
+ ### Patch Changes
134
+
135
+ - cab6a93: **plugin-grid:** column summary footer now formats values using the
136
+ column's type metadata. Currency columns render `Sum: $1,760,000.00`
137
+ instead of bare `Sum: 1,760,000`; percent columns honor `0–1` vs
138
+ `0–100` value ranges; avg uses two fraction digits. `useColumnSummary`
139
+ accepts an optional `fieldMetadata` map (typically `objectSchema.fields`)
140
+ so per-field `type`, `currency`, `defaultCurrency`, `precision` are
141
+ respected.
142
+
143
+ **plugin-gantt:** added safe-fallback `useGanttTranslation` hook. All
144
+ hardcoded toolbar `aria-label`s and the `Task Name` / `Start` / `End` /
145
+ `Today` column-header strings now flow through `t('gantt.*')`. A new
146
+ `gantt.*` section is exported from the en/zh/ja/ko/de/fr/es/pt/ru/ar
147
+ locales.
148
+
149
+ **app-shell:** `ReportView` no longer hardcodes the `Edit` button label
150
+ or the `Loading report…` fallback — they now use `common.edit` and
151
+ `common.loading`.
152
+
153
+ **i18n:** added top-level `gantt` section (with English fallbacks in
154
+ non-en/zh locales) and the `common.addToFavorites` /
155
+ `common.removeFromFavorites` keys across all ten built-in locales so
156
+ the `builtInLocales` parity tests pass.
157
+
158
+ - Updated dependencies [cab6a93]
159
+ - @object-ui/i18n@5.0.2
160
+ - @object-ui/components@5.0.2
161
+ - @object-ui/fields@5.0.2
162
+ - @object-ui/react@5.0.2
163
+ - @object-ui/layout@5.0.2
164
+ - @object-ui/types@5.0.2
165
+ - @object-ui/core@5.0.2
166
+ - @object-ui/data-objectstack@5.0.2
167
+ - @object-ui/auth@5.0.2
168
+ - @object-ui/permissions@5.0.2
169
+ - @object-ui/collaboration@5.0.2
170
+ - @object-ui/providers@5.0.2
171
+
3
172
  ## 5.0.1
4
173
 
5
174
  ### Patch Changes
@@ -9,8 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  import { useNavigate } from 'react-router-dom';
10
10
  import { useObjectTranslation } from '@object-ui/i18n';
11
11
  import { Card, CardContent, cn } from '@object-ui/components';
12
- import { Clock, ArrowUpRight } from 'lucide-react';
13
- import { getIcon } from '../../utils/getIcon';
12
+ import { Clock, ArrowUpRight, Database, FileText, LayoutDashboard, File } from 'lucide-react';
14
13
  import { capitalizeFirst } from '../../utils';
15
14
  const TYPE_TONES = {
16
15
  object: 'bg-blue-500/10 text-blue-600 dark:text-blue-400 ring-blue-500/20',
@@ -18,13 +17,21 @@ const TYPE_TONES = {
18
17
  page: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 ring-emerald-500/20',
19
18
  record: 'bg-amber-500/10 text-amber-600 dark:text-amber-400 ring-amber-500/20',
20
19
  };
20
+ // Per-type icon so the four kinds are visually distinguishable — see
21
+ // StarredApps.tsx for the rationale.
22
+ const TYPE_ICONS = {
23
+ object: Database,
24
+ record: FileText,
25
+ dashboard: LayoutDashboard,
26
+ page: File,
27
+ };
21
28
  export function RecentApps({ items }) {
22
29
  const navigate = useNavigate();
23
30
  const { t } = useObjectTranslation();
24
31
  if (items.length === 0)
25
32
  return null;
26
33
  return (_jsxs("section", { children: [_jsxs("div", { className: "flex items-center gap-2 mb-5", children: [_jsx("span", { className: "inline-flex h-8 w-8 items-center justify-center rounded-lg bg-sky-500/10 ring-1 ring-sky-500/20 text-sky-600 dark:text-sky-400", children: _jsx(Clock, { className: "h-4 w-4" }) }), _jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: t('home.recentApps.title', { defaultValue: 'Recently Accessed' }) })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3", children: items.map((item) => {
27
- const Icon = getIcon(item.type);
34
+ const Icon = TYPE_ICONS[item.type] || Database;
28
35
  const typeLabel = t(`home.recentApps.itemType.${item.type}`, {
29
36
  defaultValue: capitalizeFirst(item.type),
30
37
  });
@@ -9,8 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  import { useNavigate } from 'react-router-dom';
10
10
  import { useObjectTranslation } from '@object-ui/i18n';
11
11
  import { Card, CardContent, cn } from '@object-ui/components';
12
- import { Star, ArrowUpRight } from 'lucide-react';
13
- import { getIcon } from '../../utils/getIcon';
12
+ import { Star, ArrowUpRight, Database, FileText, LayoutDashboard, File } from 'lucide-react';
14
13
  import { capitalizeFirst } from '../../utils';
15
14
  const TYPE_TONES = {
16
15
  object: 'bg-blue-500/10 text-blue-600 dark:text-blue-400 ring-blue-500/20',
@@ -18,19 +17,35 @@ const TYPE_TONES = {
18
17
  page: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 ring-emerald-500/20',
19
18
  record: 'bg-amber-500/10 text-amber-600 dark:text-amber-400 ring-amber-500/20',
20
19
  };
20
+ // Per-type icon so the four kinds (object / record / dashboard / page) are
21
+ // visually distinguishable at a glance. `getIcon` falls back to the same
22
+ // Database glyph for every unknown name which made all cards look identical.
23
+ const TYPE_ICONS = {
24
+ object: Database,
25
+ record: FileText,
26
+ dashboard: LayoutDashboard,
27
+ page: File,
28
+ };
21
29
  export function StarredApps({ items }) {
22
30
  const navigate = useNavigate();
23
31
  const { t } = useObjectTranslation();
24
32
  if (items.length === 0)
25
33
  return null;
26
34
  return (_jsxs("section", { children: [_jsxs("div", { className: "flex items-center gap-2 mb-5", children: [_jsx("span", { className: "inline-flex h-8 w-8 items-center justify-center rounded-lg bg-amber-500/10 ring-1 ring-amber-500/20 text-amber-600 dark:text-amber-400", children: _jsx(Star, { className: "h-4 w-4 fill-current" }) }), _jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: t('home.starredApps.title', { defaultValue: 'Starred' }) })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3", children: items.map((item) => {
27
- const Icon = getIcon(item.type);
35
+ const Icon = TYPE_ICONS[item.type] || Database;
28
36
  const tone = TYPE_TONES[item.type] || TYPE_TONES.object;
37
+ // Reuse the recentApps.itemType.* keys so Starred and Recently
38
+ // Accessed surface the same localized labels (e.g. "记录" vs
39
+ // "Record"). Falls back to the capitalized english type so
40
+ // unknown types still render readably.
41
+ const typeLabel = t(`home.recentApps.itemType.${item.type}`, {
42
+ defaultValue: capitalizeFirst(item.type),
43
+ });
29
44
  return (_jsx(Card, { className: "group cursor-pointer border border-border/70 bg-card/80 backdrop-blur-sm transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-foreground/20 motion-reduce:transition-none motion-reduce:hover:transform-none", onClick: () => navigate(item.href), "data-testid": `starred-item-${item.id}`, role: "link", tabIndex: 0, onKeyDown: (e) => {
30
45
  if (e.key === 'Enter' || e.key === ' ') {
31
46
  e.preventDefault();
32
47
  navigate(item.href);
33
48
  }
34
- }, children: _jsx(CardContent, { className: "p-3.5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('inline-flex h-10 w-10 items-center justify-center rounded-lg ring-1 shrink-0', tone), children: _jsx(Icon, { className: "h-5 w-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "font-medium text-sm truncate", children: item.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: capitalizeFirst(item.type) })] }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-[opacity,transform] duration-200 group-hover:opacity-100 group-hover:translate-x-0" })] }) }) }, item.id));
49
+ }, children: _jsx(CardContent, { className: "p-3.5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('inline-flex h-10 w-10 items-center justify-center rounded-lg ring-1 shrink-0', tone), children: _jsx(Icon, { className: "h-5 w-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "font-medium text-sm truncate", children: item.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: typeLabel })] }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-[opacity,transform] duration-200 group-hover:opacity-100 group-hover:translate-x-0" })] }) }) }, item.id));
35
50
  }) })] }));
36
51
  }
@@ -33,6 +33,13 @@ interface FavoritesContextValue {
33
33
  toggleFavorite: (item: Omit<FavoriteItem, 'favoritedAt'>) => void;
34
34
  isFavorite: (id: string) => boolean;
35
35
  clearFavorites: () => void;
36
+ /**
37
+ * Self-heal a stored label for an existing favorite without re-ordering
38
+ * or resetting `favoritedAt`. Used by record pages once the human-readable
39
+ * title resolves so stale "raw id" labels (e.g. saved before the title
40
+ * loaded) get rewritten transparently on the next visit.
41
+ */
42
+ refreshLabel: (id: string, label: string) => void;
36
43
  }
37
44
  interface FavoritesProviderProps {
38
45
  children: ReactNode;
@@ -138,6 +138,24 @@ export function FavoritesProvider({ children }) {
138
138
  setFavorites([]);
139
139
  commit([]);
140
140
  }, [commit]);
141
+ const refreshLabel = useCallback((id, label) => {
142
+ if (!id || !label)
143
+ return;
144
+ setFavorites(prev => {
145
+ let changed = false;
146
+ const updated = prev.map(f => {
147
+ if (f.id === id && f.label !== label) {
148
+ changed = true;
149
+ return { ...f, label };
150
+ }
151
+ return f;
152
+ });
153
+ if (!changed)
154
+ return prev;
155
+ commit(updated);
156
+ return updated;
157
+ });
158
+ }, [commit]);
141
159
  const value = useMemo(() => ({
142
160
  favorites,
143
161
  addFavorite,
@@ -145,7 +163,8 @@ export function FavoritesProvider({ children }) {
145
163
  toggleFavorite,
146
164
  isFavorite: (id) => favorites.some(f => f.id === id),
147
165
  clearFavorites,
148
- }), [favorites, addFavorite, removeFavorite, toggleFavorite, clearFavorites]);
166
+ refreshLabel,
167
+ }), [favorites, addFavorite, removeFavorite, toggleFavorite, clearFavorites, refreshLabel]);
149
168
  return (_jsx(FavoritesContext.Provider, { value: value, children: children }));
150
169
  }
151
170
  // ---------------------------------------------------------------------------
@@ -169,6 +188,7 @@ export function useFavorites() {
169
188
  toggleFavorite: () => { },
170
189
  isFavorite: () => false,
171
190
  clearFavorites: () => { },
191
+ refreshLabel: () => { },
172
192
  };
173
193
  }
174
194
  return ctx;
@@ -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;
@@ -94,7 +94,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
94
94
  const { user, signOut, isAuthEnabled } = useAuth();
95
95
  const navigate = useNavigate();
96
96
  const { t } = useObjectTranslation();
97
- const { objectLabel: resolveNavObjectLabel } = useObjectLabel();
97
+ const { objectLabel: resolveNavObjectLabel, viewLabel: resolveNavViewLabel } = useObjectLabel();
98
98
  // Swipe-from-left-edge gesture to open sidebar on mobile
99
99
  React.useEffect(() => {
100
100
  const EDGE_THRESHOLD = 30;
@@ -200,7 +200,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
200
200
  const AreaIcon = getIcon(area.icon);
201
201
  const isActiveArea = area.id === activeAreaId;
202
202
  return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { isActive: isActiveArea, tooltip: area.label, onClick: () => setActiveAreaId(area.id), children: [_jsx(AreaIcon, { className: "h-4 w-4" }), _jsx("span", { children: area.label })] }) }, area.id));
203
- }) }) })] })), _jsx(SidebarGroup, { className: "py-0", children: _jsxs(SidebarGroupContent, { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-70" }), _jsx(SidebarInput, { placeholder: "Search navigation...", value: navSearchQuery, onChange: (e) => setNavSearchQuery(e.target.value), className: "pl-8" })] }) }), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, searchQuery: navSearchQuery, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), "Recent"] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), "Favorites"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": `Remove ${item.label} from favorites`, children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(SidebarGroup, { "data-testid": "system-fallback-nav", children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Settings, { className: "h-3.5 w-3.5" }), "System"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: systemFallbackNavigation.map((item) => {
203
+ }) }) })] })), _jsx(SidebarGroup, { className: "py-0", children: _jsxs(SidebarGroupContent, { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-70" }), _jsx(SidebarInput, { placeholder: "Search navigation...", value: navSearchQuery, onChange: (e) => setNavSearchQuery(e.target.value), className: "pl-8" })] }) }), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, searchQuery: navSearchQuery, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, fallback), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), "Recent"] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), "Favorites"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": `Remove ${item.label} from favorites`, children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(SidebarGroup, { "data-testid": "system-fallback-nav", children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Settings, { className: "h-3.5 w-3.5" }), "System"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: systemFallbackNavigation.map((item) => {
204
204
  const NavIcon = getIcon(item.icon);
205
205
  return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.url || '/system', children: [_jsx(NavIcon, { className: "h-4 w-4" }), _jsx("span", { children: item.label })] }) }) }, item.id));
206
206
  }) }) })] })) }), _jsx(SidebarFooter, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] }), _jsx(ChevronsUpDown, { className: "ml-auto size-4" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", side: isMobile ? "bottom" : "right", align: "end", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-1 py-1.5 text-left text-sm", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('user.settings', { defaultValue: 'Settings' })] }) }), isAuthEnabled && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), t('user.logout', { defaultValue: 'Log out' })] })] }))] })] }) }) }) })] }), isMobile && (_jsx("div", { className: "fixed bottom-0 left-0 right-0 z-50 flex items-center justify-around border-t bg-background/95 backdrop-blur-sm px-2 py-1 sm:hidden safe-area-bottom", children: (() => {
@@ -88,7 +88,7 @@ export function UnifiedSidebar({ activeAppName }) {
88
88
  const { isMobile, setOpenMobile } = useSidebar();
89
89
  const location = useLocation();
90
90
  const { t } = useObjectTranslation();
91
- const { objectLabel: resolveNavObjectLabel, dashboardLabel: resolveNavDashboardLabel, navGroupLabel: resolveNavGroupLabel } = useObjectLabel();
91
+ const { objectLabel: resolveNavObjectLabel, dashboardLabel: resolveNavDashboardLabel, navGroupLabel: resolveNavGroupLabel, viewLabel: resolveNavViewLabel } = useObjectLabel();
92
92
  const { context, currentAppName } = useNavigationContext();
93
93
  // Swipe-from-left-edge gesture to open sidebar on mobile
94
94
  React.useEffect(() => {
@@ -175,7 +175,7 @@ export function UnifiedSidebar({ activeAppName }) {
175
175
  const AreaIcon = getIcon(area.icon);
176
176
  const isActiveArea = area.id === activeAreaId;
177
177
  return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { isActive: isActiveArea, tooltip: area.label, onClick: () => setActiveAreaId(area.id), children: [_jsx(AreaIcon, { className: "h-4 w-4" }), _jsx("span", { children: area.label })] }) }, area.id));
178
- }) }) })] })), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, enablePinning: !isMobile, onPinToggle: togglePin, enableReorder: !isMobile, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveDashboardLabel: (dashboardName, fallback) => resolveNavDashboardLabel({ name: dashboardName, label: fallback }), resolveGroupLabel: activeApp ? (groupId, fallback) => resolveNavGroupLabel(activeApp.name, groupId, fallback) : undefined, t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), t('sidebar.recent', { defaultValue: 'Recent' })] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), t('sidebar.favorites', { defaultValue: 'Favorites' })] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": t('sidebar.removeFromFavorites', { defaultValue: 'Remove {{name}} from favorites', name: item.label }), children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(_Fragment, { children: [_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: homeNavigation.map((item) => {
178
+ }) }) })] })), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, enablePinning: !isMobile, onPinToggle: togglePin, enableReorder: !isMobile, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveDashboardLabel: (dashboardName, fallback) => resolveNavDashboardLabel({ name: dashboardName, label: fallback }), resolveGroupLabel: activeApp ? (groupId, fallback) => resolveNavGroupLabel(activeApp.name, groupId, fallback) : undefined, resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, fallback), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), t('sidebar.recent', { defaultValue: 'Recent' })] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), t('sidebar.favorites', { defaultValue: 'Favorites' })] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": t('sidebar.removeFromFavorites', { defaultValue: 'Remove {{name}} from favorites', name: item.label }), children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(_Fragment, { children: [_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: homeNavigation.map((item) => {
179
179
  const NavIcon = getIcon(item.icon);
180
180
  const isActive = location.pathname === item.url;
181
181
  return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, isActive: isActive, children: _jsxs(Link, { to: item.url || '/home', children: [_jsx(NavIcon, { className: "h-4 w-4" }), _jsx("span", { children: item.label })] }) }) }, item.id));
@@ -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,9 +197,10 @@ 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
- const { objectLabel, objectDescription: objectDesc, viewLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
203
+ const { objectLabel, objectDescription: objectDesc, viewLabel, viewEmptyState, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
203
204
  const { isFavorite, toggleFavorite } = useFavorites();
204
205
  // Inline view config panel state (Airtable-style right sidebar)
205
206
  const [showViewConfigPanel, setShowViewConfigPanel] = useState(false);
@@ -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,
@@ -1433,9 +1440,9 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
1433
1440
  showRecordCount: viewDef.showRecordCount ?? listSchema.showRecordCount,
1434
1441
  allowPrinting: viewDef.allowPrinting ?? listSchema.allowPrinting,
1435
1442
  virtualScroll: viewDef.virtualScroll ?? listSchema.virtualScroll,
1436
- emptyState: viewDef.emptyState
1443
+ emptyState: viewEmptyState(objectDef.name, viewDef.name || viewDef.id || '', viewDef.emptyState
1437
1444
  ?? listSchema.emptyState
1438
- ?? resolveManagedByEmptyState(objectDef?.managedBy),
1445
+ ?? resolveManagedByEmptyState(objectDef?.managedBy)),
1439
1446
  aria: viewDef.aria ?? listSchema.aria,
1440
1447
  tabs: listSchema.tabs,
1441
1448
  // Propagate filter/sort as default filters/sort for data flow
@@ -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,
@@ -1668,7 +1681,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
1668
1681
  { title: 'View Configuration', data: activeView },
1669
1682
  { title: 'Object Definition', data: objectDef },
1670
1683
  ] }), _jsx("div", { "data-testid": "view-config-panel-wrapper", className: `transition-[max-width,opacity] duration-300 ease-in-out overflow-hidden ${showViewConfigPanel && isAdmin ? 'max-w-[280px] opacity-100' : 'max-w-0 opacity-0'}`, children: _jsx(ViewConfigPanel, { open: showViewConfigPanel && isAdmin, onClose: () => { setShowViewConfigPanel(false); setViewConfigPanelMode('edit'); }, mode: viewConfigPanelMode, activeView: activeView, objectDef: objectDef, recordCount: recordCount, onSave: handleViewConfigSave, onViewUpdate: handleViewUpdate, onCreate: handleViewCreate }) }), _jsx(CreateViewDialog, { open: showCreateViewDialog && isAdmin, onOpenChange: setShowCreateViewDialog, existingLabels: views.map((v) => v.label).filter(Boolean), objectDef: objectDef, onCreate: (cfg) => handleViewCreate(cfg) })] }), navOverlay.mode !== 'split' && (_jsx(NavigationOverlay, { ...navOverlay, setIsOpen: (open) => { if (!open)
1671
- handleDrawerClose(); }, title: objectDef.label, onExpand: handleExpandDrawer, expandLabel: t('console.objectView.expandToPage', { defaultValue: 'Open as full page' }), storageKey: `drawer-width:${objectDef.name}`, children: (record) => {
1684
+ handleDrawerClose(); }, title: objectLabel(objectDef), onExpand: handleExpandDrawer, expandLabel: t('console.objectView.expandToPage', { defaultValue: 'Open as full page' }), storageKey: `drawer-width:${objectDef.name}`, children: (record) => {
1672
1685
  const recordId = (record.id || record._id);
1673
1686
  return (_jsx(RecordDetailView, { dataSource: dataSource, objects: objects, onEdit: onEdit, objectNameOverride: objectDef.name, recordIdOverride: recordId, embedded: true }));
1674
1687
  } }))] }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
@@ -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
- import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
13
- import { PresenceAvatars } from '@object-ui/collaboration';
12
+ import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
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, Star, StarOff } 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';
@@ -28,6 +27,7 @@ import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
28
27
  import { useRecordApprovals } from '../hooks/useRecordApprovals';
29
28
  import { getRecordDisplayName } from '../utils';
30
29
  import { useFavorites } from '../hooks/useFavorites';
30
+ import { useRecentItems } from '../hooks/useRecentItems';
31
31
  const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
32
32
  /**
33
33
  * Audit field names auto-injected by the framework's `applySystemFields`.
@@ -49,6 +49,24 @@ const AUDIT_FIELD_NAMES = new Set(['created_at', 'created_by', 'updated_at', 'up
49
49
  const HIDDEN_SYSTEM_FIELD_NAMES = new Set([
50
50
  'organization_id', 'tenant_id', 'is_deleted', 'deleted_at',
51
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
+ }
52
70
  export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverride, recordIdOverride, embedded }) {
53
71
  const params = useParams();
54
72
  const appName = params.appName;
@@ -57,12 +75,15 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
57
75
  const { showDebug } = useMetadataInspector();
58
76
  const { user } = useAuth();
59
77
  const navigate = useNavigate();
78
+ const location = useLocation();
79
+ const originFrom = location.state?.from;
60
80
  const { t } = useObjectTranslation();
61
81
  const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
62
- const { isFavorite, toggleFavorite } = useFavorites();
82
+ const { isFavorite, toggleFavorite, refreshLabel: refreshFavoriteLabel } = useFavorites();
83
+ const { addRecentItem } = useRecentItems();
63
84
  const [isLoading, setIsLoading] = useState(true);
64
85
  const [feedItems, setFeedItems] = useState([]);
65
- const [recordViewers, setRecordViewers] = useState([]);
86
+ const [mentionSuggestions, setMentionSuggestions] = useState([]);
66
87
  const [actionRefreshKey, setActionRefreshKey] = useState(0);
67
88
  const [childRelatedData, setChildRelatedData] = useState({});
68
89
  const [historyEntries, setHistoryEntries] = useState(null);
@@ -77,6 +98,21 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
77
98
  // Navigation code passes `record.id || record._id` directly into the URL
78
99
  // without adding any prefix, so no stripping is needed.
79
100
  const pureRecordId = recordId;
101
+ const favoriteRecord = useMemo(() => {
102
+ if (!objectName || !pureRecordId)
103
+ return null;
104
+ return {
105
+ id: `record:${objectName}:${pureRecordId}`,
106
+ label: recordTitle || pureRecordId || '',
107
+ href: `/apps/${appName}/${objectName}/record/${pureRecordId}`,
108
+ type: 'record',
109
+ };
110
+ }, [appName, objectName, pureRecordId, recordTitle]);
111
+ const isRecordFavorite = favoriteRecord ? isFavorite(favoriteRecord.id) : false;
112
+ const handleToggleRecordFavorite = useCallback(() => {
113
+ if (favoriteRecord)
114
+ toggleFavorite(favoriteRecord);
115
+ }, [favoriteRecord, toggleFavorite]);
80
116
  // ─── Page Assignment (Salesforce Lightning-style record Pages) ──────
81
117
  // If a PageSchema(pageType='record') is authored for this object, render
82
118
  // it via SchemaRenderer (which dispatches to the registered 'record'
@@ -134,22 +170,63 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
134
170
  // page subtitle interpolation and record:* renderers depend on this.
135
171
  const expandFields = buildExpandFields(objectDef?.fields);
136
172
  const params = expandFields.length > 0 ? { $expand: expandFields } : undefined;
137
- const findOnePromise = params
138
- ? dataSource.findOne(objectName, pureRecordId, params)
139
- : dataSource.findOne(objectName, pureRecordId);
140
- findOnePromise
141
- .then((rec) => {
142
- if (!cancelled)
143
- setPageRecord(rec);
144
- })
145
- .catch(() => {
146
- if (!cancelled)
147
- setPageRecord(null);
148
- });
173
+ const loadRecord = () => {
174
+ const findOnePromise = params
175
+ ? dataSource.findOne(objectName, pureRecordId, params)
176
+ : dataSource.findOne(objectName, pureRecordId);
177
+ findOnePromise
178
+ .then((rec) => {
179
+ if (!cancelled)
180
+ setPageRecord(rec);
181
+ })
182
+ .catch(() => {
183
+ if (!cancelled)
184
+ setPageRecord(null);
185
+ });
186
+ };
187
+ loadRecord();
188
+ // Re-sync when any descendant signals the record changed (e.g.
189
+ // DetailView recalling a pending approval). Without this listener,
190
+ // the cached `pageRecord` would stay stale and propagate `pending`
191
+ // back into nested DetailViews via context.
192
+ const onChanged = (ev) => {
193
+ const detail = ev.detail || {};
194
+ if (detail.objectName !== objectName || String(detail.recordId) !== String(pureRecordId))
195
+ return;
196
+ loadRecord();
197
+ };
198
+ window.addEventListener('objectui:record-changed', onChanged);
149
199
  return () => {
150
200
  cancelled = true;
201
+ window.removeEventListener('objectui:record-changed', onChanged);
151
202
  };
152
203
  }, [effectivePage, objectName, pureRecordId, dataSource, objectDef]);
204
+ // Schema-driven path: derive a human-readable record title from the
205
+ // loaded `pageRecord` so favourites (record:*) and the breadcrumb show
206
+ // e.g. "Acme Corporation" instead of the raw record id. The legacy
207
+ // `DetailView` path keeps using its own `onDataLoaded` callback below.
208
+ useEffect(() => {
209
+ if (!pageRecord || typeof pageRecord !== 'object' || !objectDef)
210
+ return;
211
+ const resolved = getRecordDisplayName(objectDef, pageRecord);
212
+ if (resolved && resolved !== 'Untitled' && resolved !== recordTitle) {
213
+ setRecordTitle(resolved);
214
+ }
215
+ }, [pageRecord, objectDef, recordTitle]);
216
+ // Once we have a human-readable title, (a) record this visit into the
217
+ // "Recently Accessed" rail on the home page and (b) self-heal any
218
+ // previously-favorited entry whose label was saved as the raw record id
219
+ // (because the title hadn't loaded yet at the time of the toggle).
220
+ useEffect(() => {
221
+ if (!objectName || !pureRecordId || !appName)
222
+ return;
223
+ if (!recordTitle)
224
+ return;
225
+ const favId = `record:${objectName}:${pureRecordId}`;
226
+ const href = `/apps/${appName}/${objectName}/record/${pureRecordId}`;
227
+ addRecentItem({ id: favId, label: recordTitle, href, type: 'record' });
228
+ refreshFavoriteLabel(favId, recordTitle);
229
+ }, [appName, objectName, pureRecordId, recordTitle, addRecentItem, refreshFavoriteLabel]);
153
230
  // ─── Action Provider Handlers ───────────────────────────────────────
154
231
  // Confirm dialog state (promise-based)
155
232
  const [confirmState, setConfirmState] = useState({ open: false, message: '' });
@@ -495,20 +572,56 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
495
572
  });
496
573
  return () => { cancelled = true; };
497
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]);
498
608
  // Memoize so the object identity is stable across renders — otherwise
499
609
  // any effect that depends on it (e.g. the feed loader below) would
500
610
  // re-fire every render and create an infinite request loop.
501
611
  const currentUser = useMemo(() => (user ? { id: user.id, name: user.name, avatar: user.image } : FALLBACK_USER), [user?.id, user?.name, user?.image]);
502
- // 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.
503
621
  useEffect(() => {
504
622
  if (!dataSource || !objectName || !pureRecordId)
505
623
  return;
506
624
  const threadId = `${objectName}:${pureRecordId}`;
507
- // Fetch record viewers
508
- dataSource.find('sys_presence', { $filter: { recordId: pureRecordId } })
509
- .then((res) => { if (res.data?.length)
510
- setRecordViewers(res.data); })
511
- .catch(() => { });
512
625
  // M10.10: Fetch persisted comments from sys_comment. Field names
513
626
  // are snake_case to match the platform-objects schema
514
627
  // (`packages/platform-objects/src/audit/sys-comment.object.ts`):
@@ -809,29 +922,55 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
809
922
  };
810
923
  }),
811
924
  }))
812
- : [
813
- {
814
- // Intentionally untitled: when there's only one auto-generated
815
- // section, DetailSection flattens it (no Card chrome, no
816
- // redundant "Details" heading).
817
- showBorder: false,
818
- fields: Object.keys(objectDef.fields || {})
819
- .filter(key => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden)
820
- .map(key => {
821
- const fieldDef = objectDef.fields[key];
822
- const refTarget = fieldDef.reference_to || fieldDef.reference;
823
- return {
824
- name: key,
825
- label: fieldDef.label || key,
826
- type: fieldDef.type || 'text',
827
- ...(fieldDef.options && { options: fieldDef.options }),
828
- ...(refTarget && { reference_to: refTarget }),
829
- ...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
830
- ...(fieldDef.currency && { currency: fieldDef.currency }),
831
- };
832
- }),
833
- },
834
- ];
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
+ })();
835
974
  // Audit fields (created_at/created_by/updated_at/updated_by) are NOT
836
975
  // appended as a section here — they are surfaced by `<RecordMetaFooter>`
837
976
  // (rendered by DetailView) as a single subtle line below the content,
@@ -1085,6 +1224,24 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1085
1224
  const affordances = resolveCrudAffordances(objectDef);
1086
1225
  const items = [];
1087
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
+ });
1088
1245
  items.push({
1089
1246
  name: 'sys_edit',
1090
1247
  label: t('detail.edit', { defaultValue: 'Edit' }),
@@ -1107,14 +1264,23 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1107
1264
  title: document.title,
1108
1265
  url: window.location.href,
1109
1266
  });
1110
- }
1111
- else {
1112
- await navigator.clipboard.writeText(window.location.href);
1113
- toast.success(t('detail.linkCopied', { defaultValue: 'Link copied' }));
1267
+ return;
1114
1268
  }
1115
1269
  }
1116
1270
  catch {
1117
- // user dismissed share sheet — no-op
1271
+ // user dismissed the native share sheet — no-op
1272
+ return;
1273
+ }
1274
+ // Fallback path: clipboard. Surface failure to the user so we
1275
+ // never silently no-op (e.g. when clipboard access is denied
1276
+ // because the page is not focused or running over http://).
1277
+ try {
1278
+ await navigator.clipboard.writeText(window.location.href);
1279
+ toast.success(t('detail.linkCopied', { defaultValue: 'Link copied' }));
1280
+ }
1281
+ catch (err) {
1282
+ toast.error(t('detail.linkCopyFailed', { defaultValue: 'Failed to copy link' }) +
1283
+ (err?.message ? `: ${err.message}` : ''));
1118
1284
  }
1119
1285
  },
1120
1286
  });
@@ -1124,7 +1290,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1124
1290
  label: t('detail.delete', { defaultValue: 'Delete' }),
1125
1291
  type: 'script',
1126
1292
  locations: ['record_header'],
1127
- variant: 'outline',
1293
+ variant: 'destructive',
1128
1294
  onClick: async () => {
1129
1295
  const msg = t('detail.deleteConfirmation', {
1130
1296
  defaultValue: 'Are you sure you want to delete this record?',
@@ -1183,18 +1349,14 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1183
1349
  headerActions: synthHeaderActions,
1184
1350
  related: synthRelated,
1185
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,
1186
1357
  ...(assignedSlots ? { slots: assignedSlots } : {}),
1187
1358
  });
1188
- 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: [objectName && pureRecordId && (_jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", onClick: () => toggleFavorite({
1189
- id: `record:${objectName}:${pureRecordId}`,
1190
- label: recordTitle || pureRecordId || '',
1191
- href: `/apps/${appName}/${objectName}/record/${pureRecordId}`,
1192
- type: 'record',
1193
- }), "aria-pressed": isFavorite(`record:${objectName}:${pureRecordId}`), "aria-label": isFavorite(`record:${objectName}:${pureRecordId}`)
1194
- ? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' })
1195
- : t('common.addToFavorites', { defaultValue: 'Add to favorites' }), "data-testid": `record-favorite-btn-${pureRecordId}`, children: isFavorite(`record:${objectName}:${pureRecordId}`)
1196
- ? _jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })
1197
- : _jsx(StarOff, { className: "h-4 w-4" }) })), _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, 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: {
1198
1360
  position: 'bottom',
1199
1361
  collapsible: false,
1200
1362
  feed: {
@@ -1202,7 +1364,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1202
1364
  enableThreading: true,
1203
1365
  showCommentInput: true,
1204
1366
  },
1205
- }, 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) => {
1206
1368
  if (!open)
1207
1369
  setConfirmState(s => ({ ...s, open: false }));
1208
1370
  } }), _jsx(ActionParamDialog, { state: paramState, onOpenChange: (open) => {
@@ -1210,16 +1372,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1210
1372
  setParamState(s => ({ ...s, open: false }));
1211
1373
  } })] }));
1212
1374
  }
1213
- 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: [objectName && pureRecordId && (_jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", onClick: () => toggleFavorite({
1214
- id: `record:${objectName}:${pureRecordId}`,
1215
- label: recordTitle || pureRecordId || '',
1216
- href: `/apps/${appName}/${objectName}/record/${pureRecordId}`,
1217
- type: 'record',
1218
- }), "aria-pressed": isFavorite(`record:${objectName}:${pureRecordId}`), "aria-label": isFavorite(`record:${objectName}:${pureRecordId}`)
1219
- ? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' })
1220
- : t('common.addToFavorites', { defaultValue: 'Add to favorites' }), "data-testid": `record-favorite-btn-${pureRecordId}`, children: isFavorite(`record:${objectName}:${pureRecordId}`)
1221
- ? _jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })
1222
- : _jsx(StarOff, { className: "h-4 w-4" }) })), _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 }), 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) => {
1223
1376
  if (!record || typeof record !== 'object')
1224
1377
  return;
1225
1378
  // Resolve the same way DetailView's header does, so the
@@ -352,5 +352,5 @@ export function ReportView({ dataSource }) {
352
352
  allowExport: true,
353
353
  loading: dataLoading, // Loading state for data fetching
354
354
  };
355
- return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [_jsxs("div", { className: "flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate", children: previewReport.title || previewReport.label || 'Report Viewer' }), previewReport.description && (_jsx("p", { className: "text-sm text-muted-foreground mt-1 line-clamp-2", children: previewReport.description }))] }), _jsx("div", { className: "shrink-0 flex items-center gap-1.5", children: _jsxs("button", { type: "button", onClick: handleOpenConfigPanel, className: "inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", "data-testid": "report-edit-button", children: [_jsx(Pencil, { className: "h-3.5 w-3.5" }), "Edit"] }) })] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-col sm:flex-row relative", children: [_jsx("div", { className: "flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5", children: _jsx("div", { className: "w-full shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150", children: _jsx(Suspense, { fallback: _jsx("div", { className: "p-8 text-sm text-muted-foreground", children: "Loading report\u2026" }), children: useSpecRenderer ? (_jsx("div", { className: "p-4 sm:p-6", children: _jsx(ReportRenderer, { schema: previewReport, dataSource: dataSource }) })) : (_jsx(ReportViewer, { schema: viewerSchema })) }) }) }), _jsx(Suspense, { fallback: null, children: _jsx(ReportConfigPanel, { open: configPanelOpen, onClose: handleCloseConfigPanel, config: reportConfig, onSave: handleReportConfigSave, onFieldChange: handleReportFieldChange, availableFields: availableFields, getFieldsForObject: getFieldsForObject }) }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Report Configuration', data: previewReport }] })] })] }));
355
+ return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [_jsxs("div", { className: "flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate", children: previewReport.title || previewReport.label || 'Report Viewer' }), previewReport.description && (_jsx("p", { className: "text-sm text-muted-foreground mt-1 line-clamp-2", children: previewReport.description }))] }), _jsx("div", { className: "shrink-0 flex items-center gap-1.5", children: _jsxs("button", { type: "button", onClick: handleOpenConfigPanel, className: "inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", "data-testid": "report-edit-button", children: [_jsx(Pencil, { className: "h-3.5 w-3.5" }), t('common.edit')] }) })] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-col sm:flex-row relative", children: [_jsx("div", { className: "flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5", children: _jsx("div", { className: "w-full shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150", children: _jsx(Suspense, { fallback: _jsx("div", { className: "p-8 text-sm text-muted-foreground", children: t('common.loading', { defaultValue: 'Loading…' }) }), children: useSpecRenderer ? (_jsx("div", { className: "p-4 sm:p-6", children: _jsx(ReportRenderer, { schema: previewReport, dataSource: dataSource }) })) : (_jsx(ReportViewer, { schema: viewerSchema })) }) }) }), _jsx(Suspense, { fallback: null, children: _jsx(ReportConfigPanel, { open: configPanelOpen, onClose: handleCloseConfigPanel, config: reportConfig, onSave: handleReportConfigSave, onFieldChange: handleReportFieldChange, availableFields: availableFields, getFieldsForObject: getFieldsForObject }) }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Report Configuration', data: previewReport }] })] })] }));
356
356
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "5.0.1",
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.1",
31
- "@object-ui/collaboration": "5.0.1",
32
- "@object-ui/components": "5.0.1",
33
- "@object-ui/core": "5.0.1",
34
- "@object-ui/data-objectstack": "5.0.1",
35
- "@object-ui/fields": "5.0.1",
36
- "@object-ui/i18n": "5.0.1",
37
- "@object-ui/layout": "5.0.1",
38
- "@object-ui/permissions": "5.0.1",
39
- "@object-ui/providers": "5.0.1",
40
- "@object-ui/react": "5.0.1",
41
- "@object-ui/types": "5.0.1"
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.1",
48
- "@object-ui/plugin-charts": "^5.0.1",
49
- "@object-ui/plugin-chatbot": "^5.0.1",
50
- "@object-ui/plugin-dashboard": "^5.0.1",
51
- "@object-ui/plugin-designer": "^5.0.1",
52
- "@object-ui/plugin-detail": "^5.0.1",
53
- "@object-ui/plugin-form": "^5.0.1",
54
- "@object-ui/plugin-grid": "^5.0.1",
55
- "@object-ui/plugin-kanban": "^5.0.1",
56
- "@object-ui/plugin-list": "^5.0.1",
57
- "@object-ui/plugin-report": "^5.0.1",
58
- "@object-ui/plugin-view": "^5.0.1"
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",