@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 +169 -0
- package/dist/console/home/RecentApps.js +10 -3
- package/dist/console/home/StarredApps.js +19 -4
- package/dist/context/FavoritesProvider.d.ts +7 -0
- package/dist/context/FavoritesProvider.js +21 -1
- package/dist/layout/AppHeader.js +10 -20
- package/dist/layout/AppSidebar.js +2 -2
- package/dist/layout/UnifiedSidebar.js +2 -2
- package/dist/views/ObjectView.js +24 -11
- package/dist/views/RecordDetailView.js +227 -74
- package/dist/views/ReportView.js +1 -1
- package/package.json +25 -25
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 =
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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;
|
package/dist/layout/AppHeader.js
CHANGED
|
@@ -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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 ??
|
|
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));
|
package/dist/views/ObjectView.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
|
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 [
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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: '
|
|
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: [
|
|
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: [
|
|
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
|
package/dist/views/ReportView.js
CHANGED
|
@@ -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" }),
|
|
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.
|
|
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.
|
|
31
|
-
"@object-ui/collaboration": "5.
|
|
32
|
-
"@object-ui/components": "5.
|
|
33
|
-
"@object-ui/core": "5.
|
|
34
|
-
"@object-ui/data-objectstack": "5.
|
|
35
|
-
"@object-ui/fields": "5.
|
|
36
|
-
"@object-ui/i18n": "5.
|
|
37
|
-
"@object-ui/layout": "5.
|
|
38
|
-
"@object-ui/permissions": "5.
|
|
39
|
-
"@object-ui/providers": "5.
|
|
40
|
-
"@object-ui/react": "5.
|
|
41
|
-
"@object-ui/types": "5.
|
|
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.
|
|
48
|
-
"@object-ui/plugin-charts": "^5.
|
|
49
|
-
"@object-ui/plugin-chatbot": "^5.
|
|
50
|
-
"@object-ui/plugin-dashboard": "^5.
|
|
51
|
-
"@object-ui/plugin-designer": "^5.
|
|
52
|
-
"@object-ui/plugin-detail": "^5.
|
|
53
|
-
"@object-ui/plugin-form": "^5.
|
|
54
|
-
"@object-ui/plugin-grid": "^5.
|
|
55
|
-
"@object-ui/plugin-kanban": "^5.
|
|
56
|
-
"@object-ui/plugin-list": "^5.
|
|
57
|
-
"@object-ui/plugin-report": "^5.
|
|
58
|
-
"@object-ui/plugin-view": "^5.
|
|
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",
|