@object-ui/app-shell 4.0.6 → 4.0.8
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 +136 -0
- package/dist/components/DashboardRenderer.js +4 -2
- package/dist/components/FormRenderer.js +4 -2
- package/dist/components/PageRenderer.js +4 -2
- package/dist/console/AppContent.js +5 -3
- package/dist/hooks/useNavigationSync.js +38 -36
- package/dist/hooks/useObjectActions.d.ts +11 -1
- package/dist/hooks/useObjectActions.js +44 -8
- package/dist/layout/ActivityFeed.js +21 -10
- package/dist/layout/AppHeader.js +14 -8
- package/dist/layout/AppSidebar.js +3 -3
- package/dist/layout/AppSwitcher.js +1 -1
- package/dist/layout/PageHeader.d.ts +42 -0
- package/dist/layout/PageHeader.js +13 -0
- package/dist/layout/index.d.ts +2 -0
- package/dist/layout/index.js +1 -0
- package/dist/utils/getIcon.d.ts +4 -1
- package/dist/utils/getIcon.js +11 -2
- package/dist/utils/index.d.ts +8 -1
- package/dist/utils/index.js +13 -2
- package/dist/views/ActionParamDialog.js +3 -1
- package/dist/views/CreateViewDialog.js +162 -23
- package/dist/views/DashboardView.js +75 -28
- package/dist/views/MetadataInspector.js +6 -3
- package/dist/views/ObjectView.d.ts +1 -1
- package/dist/views/ObjectView.js +296 -42
- package/dist/views/PageView.js +3 -1
- package/dist/views/RecordDetailView.js +91 -9
- package/dist/views/RecordFormPage.js +21 -2
- package/dist/views/ReportView.js +71 -17
- package/dist/views/SearchResultsPage.js +10 -5
- package/dist/views/ViewConfigPanel.js +24 -6
- package/package.json +29 -29
|
@@ -12,7 +12,7 @@ import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
|
|
|
12
12
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
13
13
|
import { PresenceAvatars } from '@object-ui/collaboration';
|
|
14
14
|
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
|
|
15
|
-
import { ActionProvider, useObjectTranslation } from '@object-ui/react';
|
|
15
|
+
import { ActionProvider, useObjectTranslation, useObjectLabel } from '@object-ui/react';
|
|
16
16
|
import { toast } from 'sonner';
|
|
17
17
|
import { Database, Users } from 'lucide-react';
|
|
18
18
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
@@ -23,11 +23,12 @@ import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
|
|
|
23
23
|
import { getRecordDisplayName } from '../utils';
|
|
24
24
|
const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
|
|
25
25
|
export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
26
|
-
const { objectName, recordId } = useParams();
|
|
26
|
+
const { appName, objectName, recordId } = useParams();
|
|
27
27
|
const { showDebug } = useMetadataInspector();
|
|
28
28
|
const { user } = useAuth();
|
|
29
29
|
const navigate = useNavigate();
|
|
30
30
|
const { t } = useObjectTranslation();
|
|
31
|
+
const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess } = useObjectLabel();
|
|
31
32
|
const [isLoading, setIsLoading] = useState(true);
|
|
32
33
|
const [feedItems, setFeedItems] = useState([]);
|
|
33
34
|
const [recordViewers, setRecordViewers] = useState([]);
|
|
@@ -368,7 +369,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
368
369
|
const formSections = objectDef.views?.form?.sections;
|
|
369
370
|
const sections = formSections && formSections.length > 0
|
|
370
371
|
? formSections.map((sec) => ({
|
|
371
|
-
title: sec.title,
|
|
372
|
+
title: sec.name ? sectionLabel(objectDef.name, sec.name, sec.title || sec.name) : sec.title,
|
|
372
373
|
collapsible: sec.collapsible,
|
|
373
374
|
defaultCollapsed: sec.defaultCollapsed,
|
|
374
375
|
fields: (sec.fields || []).map((f) => {
|
|
@@ -423,7 +424,16 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
423
424
|
return false;
|
|
424
425
|
seen.add(a.name);
|
|
425
426
|
return true;
|
|
426
|
-
})
|
|
427
|
+
}).map((a) => ({
|
|
428
|
+
...a,
|
|
429
|
+
label: actionLabel(objectDef.name, a.name, a.label || a.name),
|
|
430
|
+
...(a.confirmText !== undefined && {
|
|
431
|
+
confirmText: actionConfirm(objectDef.name, a.name, a.confirmText),
|
|
432
|
+
}),
|
|
433
|
+
...(a.successMessage !== undefined && {
|
|
434
|
+
successMessage: actionSuccess(objectDef.name, a.name, a.successMessage),
|
|
435
|
+
}),
|
|
436
|
+
}));
|
|
427
437
|
})();
|
|
428
438
|
// Build highlightFields: exclusively from objectDef metadata (no hardcoded fallback)
|
|
429
439
|
const highlightFields = objectDef.views?.detail?.highlightFields ?? [];
|
|
@@ -432,16 +442,88 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
432
442
|
// Build related entries from reverse-reference child objects.
|
|
433
443
|
// `referenceField` is the FK field on the child pointing back to this
|
|
434
444
|
// record — passed so the related-list renderer can hide the redundant
|
|
435
|
-
// parent-ID column.
|
|
445
|
+
// parent-ID column. Each entry carries action handlers that the renderer
|
|
446
|
+
// surfaces as header `+ New` / `View All` buttons and per-row Edit /
|
|
447
|
+
// Delete controls.
|
|
448
|
+
const baseAppUrl = appName ? `/apps/${appName}` : '';
|
|
436
449
|
const related = childRelations.map(({ childObject, childLabel, referenceField }) => {
|
|
437
450
|
const childObjectDef = objects.find((o) => o.name === childObject);
|
|
451
|
+
const parentId = pureRecordId || '';
|
|
452
|
+
const localizedTitle = childObjectDef
|
|
453
|
+
? objectLabel({ name: childObjectDef.name, label: childObjectDef.label || childLabel })
|
|
454
|
+
: childLabel;
|
|
455
|
+
const buildNewUrl = () => {
|
|
456
|
+
const qs = new URLSearchParams({ [referenceField]: parentId }).toString();
|
|
457
|
+
return `${baseAppUrl}/${childObject}/new${qs ? `?${qs}` : ''}`;
|
|
458
|
+
};
|
|
459
|
+
const buildListUrl = () => {
|
|
460
|
+
const qs = new URLSearchParams({
|
|
461
|
+
[`filter[${referenceField}]`]: parentId,
|
|
462
|
+
}).toString();
|
|
463
|
+
return `${baseAppUrl}/${childObject}${qs ? `?${qs}` : ''}`;
|
|
464
|
+
};
|
|
465
|
+
const buildEditUrl = (row) => {
|
|
466
|
+
const rid = row?.id || row?._id;
|
|
467
|
+
if (!rid)
|
|
468
|
+
return null;
|
|
469
|
+
return `${baseAppUrl}/${childObject}/record/${encodeURIComponent(String(rid))}/edit`;
|
|
470
|
+
};
|
|
471
|
+
const buildRecordUrl = (row) => {
|
|
472
|
+
const rid = row?.id || row?._id;
|
|
473
|
+
if (!rid)
|
|
474
|
+
return null;
|
|
475
|
+
return `${baseAppUrl}/${childObject}/record/${encodeURIComponent(String(rid))}`;
|
|
476
|
+
};
|
|
477
|
+
const onNew = baseAppUrl
|
|
478
|
+
? () => navigate(buildNewUrl())
|
|
479
|
+
: undefined;
|
|
480
|
+
const onViewAll = baseAppUrl
|
|
481
|
+
? () => navigate(buildListUrl())
|
|
482
|
+
: undefined;
|
|
483
|
+
const onRowClick = baseAppUrl
|
|
484
|
+
? (row) => {
|
|
485
|
+
const url = buildRecordUrl(row);
|
|
486
|
+
if (url)
|
|
487
|
+
navigate(url);
|
|
488
|
+
}
|
|
489
|
+
: undefined;
|
|
490
|
+
const onRowEdit = baseAppUrl
|
|
491
|
+
? (row) => {
|
|
492
|
+
const url = buildEditUrl(row);
|
|
493
|
+
if (url)
|
|
494
|
+
navigate(url);
|
|
495
|
+
}
|
|
496
|
+
: undefined;
|
|
497
|
+
const onRowDelete = dataSource && parentId
|
|
498
|
+
? async (row) => {
|
|
499
|
+
const rid = row?.id || row?._id;
|
|
500
|
+
if (!rid)
|
|
501
|
+
return;
|
|
502
|
+
try {
|
|
503
|
+
await dataSource.delete(childObject, rid);
|
|
504
|
+
toast.success(t('detail.deleteSuccess', { defaultValue: 'Deleted' }));
|
|
505
|
+
setChildRelatedData((prev) => ({
|
|
506
|
+
...prev,
|
|
507
|
+
[childObject]: (prev[childObject] || []).filter((r) => (r.id || r._id) !== rid),
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
toast.error(err?.message || t('detail.deleteError', { defaultValue: 'Delete failed' }));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
: undefined;
|
|
438
515
|
return {
|
|
439
|
-
title:
|
|
516
|
+
title: localizedTitle,
|
|
440
517
|
type: 'table',
|
|
441
518
|
api: childObject,
|
|
442
519
|
data: childRelatedData[childObject] || [],
|
|
443
520
|
referenceField,
|
|
444
521
|
icon: childObjectDef?.icon,
|
|
522
|
+
onNew,
|
|
523
|
+
onViewAll,
|
|
524
|
+
onRowClick,
|
|
525
|
+
onRowEdit,
|
|
526
|
+
onRowDelete,
|
|
445
527
|
};
|
|
446
528
|
});
|
|
447
529
|
return {
|
|
@@ -468,14 +550,14 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
468
550
|
}),
|
|
469
551
|
};
|
|
470
552
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
471
|
-
}, [objectDef?.name, pureRecordId, childRelatedData, actionRefreshKey]);
|
|
553
|
+
}, [objectDef?.name, pureRecordId, childRelatedData, actionRefreshKey, appName, navigate, dataSource, t, objectLabel, objects]);
|
|
472
554
|
if (isLoading) {
|
|
473
555
|
return _jsx(SkeletonDetail, {});
|
|
474
556
|
}
|
|
475
557
|
if (!objectDef) {
|
|
476
|
-
return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children:
|
|
558
|
+
return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.objectNotFound') }), _jsx(EmptyDescription, { children: t('empty.objectNotFoundDescription', { name: objectName }) })] }) }));
|
|
477
559
|
}
|
|
478
|
-
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: recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title:
|
|
560
|
+
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: 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 }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), onDataLoaded: (record) => {
|
|
479
561
|
if (!record || typeof record !== 'object')
|
|
480
562
|
return;
|
|
481
563
|
// Resolve the same way DetailView's header does, so the
|
|
@@ -29,7 +29,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
29
29
|
* @module views/RecordFormPage
|
|
30
30
|
*/
|
|
31
31
|
import { useCallback, useMemo } from 'react';
|
|
32
|
-
import { useParams, useNavigate, Link } from 'react-router-dom';
|
|
32
|
+
import { useParams, useNavigate, useSearchParams, Link } from 'react-router-dom';
|
|
33
33
|
import { ObjectForm } from '@object-ui/plugin-form';
|
|
34
34
|
import { Button, Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
35
35
|
import { ArrowLeft, Database } from 'lucide-react';
|
|
@@ -52,11 +52,29 @@ import { ExpressionEvaluator } from '@object-ui/core';
|
|
|
52
52
|
export function RecordFormPage({ mode }) {
|
|
53
53
|
const { appName, objectName, recordId } = useParams();
|
|
54
54
|
const navigate = useNavigate();
|
|
55
|
+
const [searchParams] = useSearchParams();
|
|
55
56
|
const dataSource = useAdapter();
|
|
56
57
|
const { objects, loading: metadataLoading } = useMetadata();
|
|
57
58
|
const { t } = useObjectTranslation();
|
|
58
59
|
const { objectLabel } = useObjectLabel();
|
|
59
60
|
const { user } = useAuth();
|
|
61
|
+
/**
|
|
62
|
+
* Query-string prefills for create mode. Used by related-list "+ New"
|
|
63
|
+
* buttons that pass the parent record id as a `<referenceField>=<id>`
|
|
64
|
+
* pair so the new child record is auto-linked back to the parent.
|
|
65
|
+
* Stable identity: only changes when the actual search string changes.
|
|
66
|
+
*/
|
|
67
|
+
const prefillValues = useMemo(() => {
|
|
68
|
+
if (mode !== 'create')
|
|
69
|
+
return undefined;
|
|
70
|
+
const entries = [];
|
|
71
|
+
for (const [k, v] of searchParams.entries()) {
|
|
72
|
+
if (k && v)
|
|
73
|
+
entries.push([k, v]);
|
|
74
|
+
}
|
|
75
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
76
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
+
}, [mode, searchParams.toString()]);
|
|
60
78
|
const objectDef = useMemo(() => objects.find((o) => o.name === objectName), [objects, objectName]);
|
|
61
79
|
const baseUrl = `/apps/${appName}`;
|
|
62
80
|
const objectListUrl = `${baseUrl}/${objectName}`;
|
|
@@ -141,7 +159,7 @@ export function RecordFormPage({ mode }) {
|
|
|
141
159
|
return _jsx(SkeletonDetail, {});
|
|
142
160
|
}
|
|
143
161
|
if (!objectDef) {
|
|
144
|
-
return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children:
|
|
162
|
+
return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.objectNotFound') }), _jsx(EmptyDescription, { children: t('empty.objectNotFoundDescription', { name: objectName }) }), _jsx("div", { className: "mt-4", children: _jsxs(Button, { variant: "outline", onClick: () => navigate(baseUrl), children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), t('empty.back')] }) })] }) }));
|
|
145
163
|
}
|
|
146
164
|
return (_jsx(ExpressionProvider, { user: expressionUser, app: { name: appName }, data: {}, children: _jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", "data-testid": "record-form-page", "data-mode": mode, children: [_jsxs("header", { className: "sticky top-0 z-10 flex items-center gap-3 border-b bg-background px-4 py-3 sm:px-6", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: goBack, "data-testid": "record-form-page-back", "aria-label": t('common.back', { defaultValue: 'Back' }), children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Link, { to: objectListUrl, className: "hover:text-foreground transition-colors", children: label }), _jsx("span", { "aria-hidden": "true", children: "/" }), _jsx("span", { className: "text-foreground font-medium", "data-testid": "record-form-page-title", children: pageTitle })] })] }), _jsx("div", { className: "flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto max-w-4xl", children: _jsx(ObjectForm, { schema: {
|
|
147
165
|
type: 'object-form',
|
|
@@ -149,6 +167,7 @@ export function RecordFormPage({ mode }) {
|
|
|
149
167
|
objectName: objectDef.name,
|
|
150
168
|
mode,
|
|
151
169
|
recordId: mode === 'edit' ? recordId : undefined,
|
|
170
|
+
...(prefillValues && { initialValues: prefillValues }),
|
|
152
171
|
title: pageTitle,
|
|
153
172
|
description: mode === 'create'
|
|
154
173
|
? t('form.createDescription', {
|
package/dist/views/ReportView.js
CHANGED
|
@@ -5,6 +5,7 @@ const ReportViewer = lazy(() => import('@object-ui/plugin-report').then((m) => (
|
|
|
5
5
|
const ReportConfigPanel = lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportConfigPanel })));
|
|
6
6
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
7
7
|
import { Pencil, BarChart3, Loader2 } from 'lucide-react';
|
|
8
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
8
9
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
9
10
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
10
11
|
import { useAdapter } from '../providers/AdapterProvider';
|
|
@@ -20,6 +21,7 @@ const FALLBACK_FIELDS = [
|
|
|
20
21
|
{ value: 'amount', label: 'Amount', type: 'number' },
|
|
21
22
|
];
|
|
22
23
|
export function ReportView({ dataSource }) {
|
|
24
|
+
const { t } = useObjectTranslation();
|
|
23
25
|
const { reportName } = useParams();
|
|
24
26
|
const { showDebug } = useMetadataInspector();
|
|
25
27
|
const adapter = useAdapter();
|
|
@@ -194,7 +196,7 @@ export function ReportView({ dataSource }) {
|
|
|
194
196
|
}
|
|
195
197
|
if (!initialReport || !reportData) {
|
|
196
198
|
if (!loading && !initialReport) {
|
|
197
|
-
return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(BarChart3, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children:
|
|
199
|
+
return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(BarChart3, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.reportNotFound') }), _jsx(EmptyDescription, { children: t('empty.reportNotFoundDescription', { name: reportName }) })] }) }));
|
|
198
200
|
}
|
|
199
201
|
return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-8 w-8 animate-spin text-muted-foreground" }) }));
|
|
200
202
|
}
|
|
@@ -204,22 +206,66 @@ export function ReportView({ dataSource }) {
|
|
|
204
206
|
// Map @objectstack/spec report format to @object-ui/types ReportSchema:
|
|
205
207
|
// - 'label' → 'title'
|
|
206
208
|
// - 'columns' (with 'field') → 'fields' (with 'name') + auto-generate 'sections'
|
|
209
|
+
// - Hydrate type/options/referenceTo from the bound object's field metadata
|
|
210
|
+
// so the type-aware cell renderer can show select badges, lookup links,
|
|
211
|
+
// boolean ✓/✗, email/url/phone links, etc. instead of raw values.
|
|
207
212
|
const mapReportForViewer = (src) => {
|
|
208
213
|
const mapped = { ...src };
|
|
209
214
|
if (!mapped.title && mapped.label) {
|
|
210
215
|
mapped.title = mapped.label;
|
|
211
216
|
}
|
|
217
|
+
// Build a lookup of object-field metadata to hydrate column type info.
|
|
218
|
+
const objName = mapped.objectName || mapped.dataSource?.object || mapped.dataSource?.resource;
|
|
219
|
+
const objDef = objName ? objects?.find((o) => o.name === objName) : null;
|
|
220
|
+
const objFieldsArr = Array.isArray(objDef?.fields)
|
|
221
|
+
? objDef.fields
|
|
222
|
+
: objDef?.fields
|
|
223
|
+
? Object.entries(objDef.fields).map(([name, def]) => ({ name, ...def }))
|
|
224
|
+
: [];
|
|
225
|
+
const objFieldMap = {};
|
|
226
|
+
for (const f of objFieldsArr) {
|
|
227
|
+
if (f && f.name)
|
|
228
|
+
objFieldMap[f.name] = f;
|
|
229
|
+
}
|
|
230
|
+
const hydrate = (col) => {
|
|
231
|
+
const name = col.name || col.field;
|
|
232
|
+
const meta = name ? objFieldMap[name] : undefined;
|
|
233
|
+
if (!meta)
|
|
234
|
+
return col;
|
|
235
|
+
// Author-provided values win; only fill in what's missing.
|
|
236
|
+
const out = { ...col };
|
|
237
|
+
if (out.type === undefined && meta.type !== undefined)
|
|
238
|
+
out.type = meta.type;
|
|
239
|
+
if (out.options === undefined && Array.isArray(meta.options))
|
|
240
|
+
out.options = meta.options;
|
|
241
|
+
if (out.referenceTo === undefined) {
|
|
242
|
+
const ref = meta.referenceTo || meta.reference?.to || meta.target;
|
|
243
|
+
if (ref)
|
|
244
|
+
out.referenceTo = ref;
|
|
245
|
+
}
|
|
246
|
+
if (out.label === undefined && meta.label)
|
|
247
|
+
out.label = meta.label;
|
|
248
|
+
return out;
|
|
249
|
+
};
|
|
212
250
|
// Map spec 'columns' (field/label/aggregate) → ReportSchema 'fields' (name/label/aggregation)
|
|
213
251
|
if (!mapped.fields && Array.isArray(mapped.columns)) {
|
|
214
|
-
mapped.fields = mapped.columns.map((col) =>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
252
|
+
mapped.fields = mapped.columns.map((col) => {
|
|
253
|
+
const hydrated = hydrate(col);
|
|
254
|
+
return {
|
|
255
|
+
name: hydrated.field || hydrated.name,
|
|
256
|
+
label: hydrated.label,
|
|
257
|
+
type: hydrated.type,
|
|
258
|
+
options: hydrated.options,
|
|
259
|
+
referenceTo: hydrated.referenceTo,
|
|
260
|
+
format: hydrated.format,
|
|
261
|
+
renderAs: hydrated.renderAs,
|
|
262
|
+
colorMap: hydrated.colorMap,
|
|
263
|
+
...(hydrated.aggregate ? { aggregation: hydrated.aggregate, showInSummary: true } : {}),
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
else if (Array.isArray(mapped.fields)) {
|
|
268
|
+
mapped.fields = mapped.fields.map(hydrate);
|
|
223
269
|
}
|
|
224
270
|
// Always regenerate sections from current fields so that live config
|
|
225
271
|
// changes (e.g. field picker updates) are immediately reflected in
|
|
@@ -227,7 +273,8 @@ export function ReportView({ dataSource }) {
|
|
|
227
273
|
// did not update the rendered report.
|
|
228
274
|
if (mapped.fields && Array.isArray(mapped.fields) && mapped.fields.length > 0) {
|
|
229
275
|
const hasSummaryFields = mapped.fields.some((f) => f.showInSummary || f.aggregation);
|
|
230
|
-
|
|
276
|
+
// Spec key is `type`; legacy renderer used `reportType`. Accept either.
|
|
277
|
+
const reportType = mapped.type || mapped.reportType || 'tabular';
|
|
231
278
|
const sections = [];
|
|
232
279
|
if (reportType === 'summary' || hasSummaryFields) {
|
|
233
280
|
sections.push({ type: 'summary', title: 'Key Metrics' });
|
|
@@ -239,27 +286,34 @@ export function ReportView({ dataSource }) {
|
|
|
239
286
|
name: f.name,
|
|
240
287
|
label: f.label,
|
|
241
288
|
type: f.type,
|
|
289
|
+
options: f.options,
|
|
290
|
+
referenceTo: f.referenceTo,
|
|
242
291
|
format: f.format,
|
|
243
292
|
renderAs: f.renderAs,
|
|
244
293
|
colorMap: f.colorMap,
|
|
245
294
|
})),
|
|
246
295
|
});
|
|
247
|
-
// Generate chart section from
|
|
248
|
-
|
|
296
|
+
// Generate chart section from chart config if configured.
|
|
297
|
+
// Spec keys: type / xAxis / yAxis. Legacy: chartType / xAxisField / yAxisFields[0].
|
|
298
|
+
const chartCfg = mapped.chart || mapped.chartConfig;
|
|
299
|
+
const chartTypeVal = chartCfg?.type || chartCfg?.chartType;
|
|
300
|
+
if (chartTypeVal) {
|
|
301
|
+
const xField = chartCfg.xAxis || chartCfg.xAxisField;
|
|
302
|
+
const yField = chartCfg.yAxis || chartCfg.yAxisFields?.[0];
|
|
249
303
|
sections.push({
|
|
250
304
|
type: 'chart',
|
|
251
305
|
title: 'Chart',
|
|
252
306
|
chart: {
|
|
253
307
|
type: 'chart',
|
|
254
|
-
chartType:
|
|
255
|
-
xAxisField:
|
|
256
|
-
yAxisFields:
|
|
308
|
+
chartType: chartTypeVal,
|
|
309
|
+
xAxisField: xField,
|
|
310
|
+
yAxisFields: yField ? [yField] : chartCfg.yAxisFields,
|
|
257
311
|
},
|
|
258
312
|
});
|
|
259
313
|
}
|
|
260
314
|
// Preserve any user-defined chart sections from the original schema
|
|
261
315
|
if (Array.isArray(src.sections)) {
|
|
262
|
-
const chartSections = src.sections.filter((s) => s.type === 'chart' && !
|
|
316
|
+
const chartSections = src.sections.filter((s) => s.type === 'chart' && !chartTypeVal);
|
|
263
317
|
sections.push(...chartSections);
|
|
264
318
|
}
|
|
265
319
|
mapped.sections = sections;
|
|
@@ -11,6 +11,7 @@ import { useState, useMemo } from 'react';
|
|
|
11
11
|
import { useSearchParams, Link, useParams } from 'react-router-dom';
|
|
12
12
|
import { Input, Card, CardContent, Badge, } from '@object-ui/components';
|
|
13
13
|
import { Search, Database, LayoutDashboard, FileText, BarChart3, ArrowLeft, } from 'lucide-react';
|
|
14
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
14
15
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
15
16
|
/** Flatten nested navigation groups into a flat list of leaf items */
|
|
16
17
|
function flattenNavigation(items) {
|
|
@@ -38,6 +39,7 @@ const TYPE_COLORS = {
|
|
|
38
39
|
report: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
|
39
40
|
};
|
|
40
41
|
export function SearchResultsPage() {
|
|
42
|
+
const { t } = useObjectTranslation();
|
|
41
43
|
const { appName } = useParams();
|
|
42
44
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
43
45
|
const queryParam = searchParams.get('q') || '';
|
|
@@ -95,13 +97,16 @@ export function SearchResultsPage() {
|
|
|
95
97
|
}
|
|
96
98
|
return groups;
|
|
97
99
|
}, [results]);
|
|
98
|
-
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6 max-w-4xl mx-auto", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Link, { to: baseUrl, className: "flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }),
|
|
99
|
-
?
|
|
100
|
-
:
|
|
100
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6 max-w-4xl mx-auto", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Link, { to: baseUrl, className: "flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), t('search.back')] }), _jsx("h1", { className: "text-xl font-semibold", children: t('search.title') })] }), _jsxs("div", { className: "relative", children: [_jsx("label", { htmlFor: "search-results-input", className: "sr-only", children: t('search.placeholder') }), _jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { id: "search-results-input", value: query, onChange: (e) => handleSearch(e.target.value), placeholder: t('search.placeholder'), className: "pl-10 h-11 text-base", autoFocus: true })] }), _jsx("div", { className: "text-sm text-muted-foreground", children: query.trim()
|
|
101
|
+
? t(results.length === 1 ? 'search.resultsCount' : 'search.resultsCountPlural', { count: results.length, query })
|
|
102
|
+
: t('search.itemsAvailable', { count: allItems.length }) }), results.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: [_jsx(Search, { className: "h-12 w-12 text-muted-foreground/30 mb-4" }), _jsx("p", { className: "text-lg font-medium text-muted-foreground", children: t('search.noResults') }), _jsx("p", { className: "text-sm text-muted-foreground/80 mt-1", children: t('search.noResultsHint') })] })) : (_jsx("div", { className: "space-y-6", children: Object.entries(grouped).map(([type, items]) => {
|
|
101
103
|
const TypeIcon = TYPE_ICONS[type] || Database;
|
|
102
|
-
|
|
104
|
+
const typeLabelKey = `search.type${type.charAt(0).toUpperCase()}${type.slice(1)}s`;
|
|
105
|
+
const badgeKey = `search.badge${type.charAt(0).toUpperCase()}${type.slice(1)}`;
|
|
106
|
+
return (_jsxs("div", { children: [_jsxs("h2", { className: "text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1.5", children: [_jsx(TypeIcon, { className: "h-4 w-4" }), t(typeLabelKey), _jsx(Badge, { variant: "secondary", className: "ml-1 text-xs", children: items.length })] }), _jsx("div", { className: "grid gap-2", children: items.map(item => {
|
|
103
107
|
const ItemIcon = TYPE_ICONS[item.type] || Database;
|
|
104
|
-
|
|
108
|
+
const itemBadgeKey = `search.badge${item.type.charAt(0).toUpperCase()}${item.type.slice(1)}`;
|
|
109
|
+
return (_jsx(Link, { to: item.href, className: "rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: _jsx(Card, { className: "hover:bg-accent/50 transition-colors cursor-pointer", children: _jsxs(CardContent, { className: "flex items-center gap-3 p-3", children: [_jsx("div", { className: `flex h-8 w-8 items-center justify-center rounded ${TYPE_COLORS[item.type] || ''}`, children: _jsx(ItemIcon, { className: "h-4 w-4" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-medium truncate", children: item.label }), item.description && (_jsx("p", { className: "text-xs text-muted-foreground truncate", children: item.description }))] }), _jsx(Badge, { variant: "outline", className: "text-xs shrink-0", children: t(itemBadgeKey) })] }) }) }, item.id));
|
|
105
110
|
}) })] }, type));
|
|
106
111
|
}) }))] }));
|
|
107
112
|
}
|
|
@@ -12,13 +12,23 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
12
12
|
* All changes are buffered in a local draft state. Clicking Save commits
|
|
13
13
|
* the draft via onSave; Discard resets to the original activeView.
|
|
14
14
|
*/
|
|
15
|
-
import { useMemo, useEffect, useRef, useCallback } from 'react';
|
|
16
|
-
import { ConfigPanelRenderer, useConfigDraft } from '@object-ui/components';
|
|
15
|
+
import { useMemo, useEffect, useRef, useCallback, useState } from 'react';
|
|
16
|
+
import { ConfigPanelRenderer, useConfigDraft, Button } from '@object-ui/components';
|
|
17
|
+
import { Settings2 } from 'lucide-react';
|
|
17
18
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
18
19
|
import { buildViewConfigSchema, deriveFieldOptions, toFilterGroup, toSortItems, VIEW_TYPE_LABELS, } from '@object-ui/plugin-view';
|
|
19
20
|
export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, objectDef, onViewUpdate, onSave, onCreate }) {
|
|
20
21
|
const { t } = useObjectTranslation();
|
|
21
22
|
const panelRef = useRef(null);
|
|
23
|
+
// "Show advanced settings" — when false (default), the panel only shows
|
|
24
|
+
// the Airtable-essential subset. When true, every section/field is
|
|
25
|
+
// surfaced for power users. Reset whenever the panel closes so reopening
|
|
26
|
+
// returns to the simplified view.
|
|
27
|
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!open)
|
|
30
|
+
setShowAdvanced(false);
|
|
31
|
+
}, [open]);
|
|
22
32
|
// Default empty view for create mode
|
|
23
33
|
const defaultNewView = useMemo(() => ({
|
|
24
34
|
id: `view_${Date.now()}`,
|
|
@@ -53,7 +63,8 @@ export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, obje
|
|
|
53
63
|
// Bridge: filter/sort → builder format
|
|
54
64
|
const filterGroupValue = useMemo(() => toFilterGroup(draft.filter), [draft.filter]);
|
|
55
65
|
const sortItemsValue = useMemo(() => toSortItems(draft.sort), [draft.sort]);
|
|
56
|
-
// Build schema
|
|
66
|
+
// Build schema. essentialOnly is the default; the user can opt-in to the
|
|
67
|
+
// full advanced surface via the "Show advanced settings" toggle below.
|
|
57
68
|
const schema = useMemo(() => buildViewConfigSchema({
|
|
58
69
|
t,
|
|
59
70
|
fieldOptions,
|
|
@@ -61,8 +72,8 @@ export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, obje
|
|
|
61
72
|
updateField,
|
|
62
73
|
filterGroupValue,
|
|
63
74
|
sortItemsValue,
|
|
64
|
-
essentialOnly:
|
|
65
|
-
}), [t, fieldOptions, objectDef, updateField, filterGroupValue, sortItemsValue]);
|
|
75
|
+
essentialOnly: !showAdvanced,
|
|
76
|
+
}), [t, fieldOptions, objectDef, updateField, filterGroupValue, sortItemsValue, showAdvanced]);
|
|
66
77
|
// Override breadcrumb with dynamic view type
|
|
67
78
|
const viewType = draft.type || 'grid';
|
|
68
79
|
const dynamicSchema = useMemo(() => ({
|
|
@@ -90,5 +101,12 @@ export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, obje
|
|
|
90
101
|
const panelTitle = mode === 'create'
|
|
91
102
|
? t('console.objectView.createView')
|
|
92
103
|
: t('console.objectView.configureView');
|
|
93
|
-
|
|
104
|
+
// Header-extra: "Show advanced" toggle button. Subtle gear icon button
|
|
105
|
+
// sitting next to undo/redo/close — clearly affordant but not visually
|
|
106
|
+
// dominant. Title swaps between "Show advanced settings" and "Show fewer
|
|
107
|
+
// settings" so the user always knows what the next click will do.
|
|
108
|
+
const advancedToggle = (_jsx(Button, { size: "sm", variant: showAdvanced ? 'secondary' : 'ghost', onClick: () => setShowAdvanced(v => !v), className: "h-7 w-7 p-0", "data-testid": "view-config-advanced-toggle", "aria-pressed": showAdvanced, title: showAdvanced
|
|
109
|
+
? t('console.objectView.showFewerSettings', { defaultValue: 'Show fewer settings' })
|
|
110
|
+
: t('console.objectView.showAdvancedSettings', { defaultValue: 'Show advanced settings' }), children: _jsx(Settings2, { className: "h-3.5 w-3.5" }) }));
|
|
111
|
+
return (_jsx(ConfigPanelRenderer, { open: open, onClose: onClose, schema: dynamicSchema, draft: draft, isDirty: isDirty, onFieldChange: updateField, onSave: handleSave, onDiscard: handleDiscard, onUndo: undo, onRedo: redo, canUndo: canUndo, canRedo: canRedo, undoLabel: t('designer.undo'), redoLabel: t('designer.redo'), objectDef: objectDef, saveLabel: t('console.objectView.save'), discardLabel: t('console.objectView.discard'), panelRef: panelRef, role: "complementary", ariaLabel: panelTitle, tabIndex: -1, testId: "view-config-panel", closeTitle: t('console.objectView.closePanel'), footerTestId: "view-config-footer", saveTestId: "view-config-save", discardTestId: "view-config-discard", headerExtra: advancedToggle, className: "transition-all overflow-hidden" }));
|
|
94
112
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/app-shell",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
|
|
@@ -27,45 +27,45 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"lucide-react": "^1.14.0",
|
|
29
29
|
"sonner": "^2.0.7",
|
|
30
|
-
"@object-ui/auth": "4.0.
|
|
31
|
-
"@object-ui/collaboration": "4.0.
|
|
32
|
-
"@object-ui/components": "4.0.
|
|
33
|
-
"@object-ui/core": "4.0.
|
|
34
|
-
"@object-ui/data-objectstack": "4.0.
|
|
35
|
-
"@object-ui/fields": "4.0.
|
|
36
|
-
"@object-ui/i18n": "4.0.
|
|
37
|
-
"@object-ui/layout": "4.0.
|
|
38
|
-
"@object-ui/permissions": "4.0.
|
|
39
|
-
"@object-ui/react": "4.0.
|
|
40
|
-
"@object-ui/types": "4.0.
|
|
30
|
+
"@object-ui/auth": "4.0.8",
|
|
31
|
+
"@object-ui/collaboration": "4.0.8",
|
|
32
|
+
"@object-ui/components": "4.0.8",
|
|
33
|
+
"@object-ui/core": "4.0.8",
|
|
34
|
+
"@object-ui/data-objectstack": "4.0.8",
|
|
35
|
+
"@object-ui/fields": "4.0.8",
|
|
36
|
+
"@object-ui/i18n": "4.0.8",
|
|
37
|
+
"@object-ui/layout": "4.0.8",
|
|
38
|
+
"@object-ui/permissions": "4.0.8",
|
|
39
|
+
"@object-ui/react": "4.0.8",
|
|
40
|
+
"@object-ui/types": "4.0.8"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0",
|
|
44
44
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
45
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
46
|
-
"@object-ui/plugin-calendar": "4.0.
|
|
47
|
-
"@object-ui/plugin-charts": "4.0.
|
|
48
|
-
"@object-ui/plugin-chatbot": "4.0.
|
|
49
|
-
"@object-ui/plugin-dashboard": "4.0.
|
|
50
|
-
"@object-ui/plugin-designer": "4.0.
|
|
51
|
-
"@object-ui/plugin-detail": "4.0.
|
|
52
|
-
"@object-ui/plugin-form": "4.0.
|
|
53
|
-
"@object-ui/plugin-grid": "4.0.
|
|
54
|
-
"@object-ui/plugin-kanban": "4.0.
|
|
55
|
-
"@object-ui/plugin-list": "4.0.
|
|
56
|
-
"@object-ui/plugin-report": "4.0.
|
|
57
|
-
"@object-ui/plugin-view": "4.0.
|
|
46
|
+
"@object-ui/plugin-calendar": "4.0.8",
|
|
47
|
+
"@object-ui/plugin-charts": "4.0.8",
|
|
48
|
+
"@object-ui/plugin-chatbot": "4.0.8",
|
|
49
|
+
"@object-ui/plugin-dashboard": "4.0.8",
|
|
50
|
+
"@object-ui/plugin-designer": "4.0.8",
|
|
51
|
+
"@object-ui/plugin-detail": "4.0.8",
|
|
52
|
+
"@object-ui/plugin-form": "4.0.8",
|
|
53
|
+
"@object-ui/plugin-grid": "4.0.8",
|
|
54
|
+
"@object-ui/plugin-kanban": "4.0.8",
|
|
55
|
+
"@object-ui/plugin-list": "4.0.8",
|
|
56
|
+
"@object-ui/plugin-report": "4.0.8",
|
|
57
|
+
"@object-ui/plugin-view": "4.0.8"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@types/node": "^25.
|
|
60
|
+
"@types/node": "^25.7.0",
|
|
61
61
|
"@types/react": "19.2.14",
|
|
62
62
|
"@types/react-dom": "19.2.3",
|
|
63
|
-
"react": "19.2.
|
|
64
|
-
"react-dom": "19.2.
|
|
65
|
-
"react-router-dom": "^7.
|
|
63
|
+
"react": "19.2.6",
|
|
64
|
+
"react-dom": "19.2.6",
|
|
65
|
+
"react-router-dom": "^7.15.0",
|
|
66
66
|
"sonner": "^2.0.7",
|
|
67
67
|
"typescript": "^6.0.3",
|
|
68
|
-
"vite": "^8.0.
|
|
68
|
+
"vite": "^8.0.12"
|
|
69
69
|
},
|
|
70
70
|
"keywords": [
|
|
71
71
|
"objectui",
|