@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.
@@ -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: childLabel,
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: "Object Not Found" }), _jsxs(EmptyDescription, { children: ["Object \"", objectName, "\" definition missing. Check your configuration or navigate back to select a valid object."] })] }) }));
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: "Users viewing this record", 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: objectDef.label, onDataLoaded: (record) => {
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: "Object Not Found" }), _jsxs(EmptyDescription, { children: ["Object \"", objectName, "\" definition missing. Check your configuration or navigate back to select a valid object."] }), _jsx("div", { className: "mt-4", children: _jsxs(Button, { variant: "outline", onClick: () => navigate(baseUrl), children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), "Back"] }) })] }) }));
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', {
@@ -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: "Report Not Found" }), _jsxs(EmptyDescription, { children: ["The report \"", reportName, "\" could not be found. It may have been removed or renamed."] })] }) }));
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
- name: col.field || col.name,
216
- label: col.label,
217
- type: col.type,
218
- format: col.format,
219
- renderAs: col.renderAs,
220
- colorMap: col.colorMap,
221
- ...(col.aggregate ? { aggregation: col.aggregate, showInSummary: true } : {}),
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
- const reportType = mapped.reportType || 'tabular';
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 chartConfig if configured
248
- if (mapped.chartConfig?.chartType) {
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: mapped.chartConfig.chartType,
255
- xAxisField: mapped.chartConfig.xAxisField,
256
- yAxisFields: mapped.chartConfig.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' && !mapped.chartConfig?.chartType);
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" }), "Back"] }), _jsx("h1", { className: "text-xl font-semibold", children: "Search" })] }), _jsxs("div", { className: "relative", children: [_jsx("label", { htmlFor: "search-results-input", className: "sr-only", children: "Search objects, dashboards, pages, reports" }), _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: "Search objects, dashboards, pages, reports...", className: "pl-10 h-11 text-base", autoFocus: true })] }), _jsx("div", { className: "text-sm text-muted-foreground", children: query.trim()
99
- ? `${results.length} result${results.length !== 1 ? 's' : ''} for "${query}"`
100
- : `${allItems.length} items available` }), 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: "No results found" }), _jsx("p", { className: "text-sm text-muted-foreground/80 mt-1", children: "Try adjusting your search terms" })] })) : (_jsx("div", { className: "space-y-6", children: Object.entries(grouped).map(([type, items]) => {
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
- 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" }), type.charAt(0).toUpperCase() + type.slice(1), "s", _jsx(Badge, { variant: "secondary", className: "ml-1 text-xs", children: items.length })] }), _jsx("div", { className: "grid gap-2", children: items.map(item => {
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
- 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: item.type })] }) }) }, item.id));
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 always essentials-only (Airtable-style focused layout).
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: true,
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
- 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", className: "transition-all overflow-hidden" }));
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.6",
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.6",
31
- "@object-ui/collaboration": "4.0.6",
32
- "@object-ui/components": "4.0.6",
33
- "@object-ui/core": "4.0.6",
34
- "@object-ui/data-objectstack": "4.0.6",
35
- "@object-ui/fields": "4.0.6",
36
- "@object-ui/i18n": "4.0.6",
37
- "@object-ui/layout": "4.0.6",
38
- "@object-ui/permissions": "4.0.6",
39
- "@object-ui/react": "4.0.6",
40
- "@object-ui/types": "4.0.6"
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.6",
47
- "@object-ui/plugin-charts": "4.0.6",
48
- "@object-ui/plugin-chatbot": "4.0.6",
49
- "@object-ui/plugin-dashboard": "4.0.6",
50
- "@object-ui/plugin-designer": "4.0.6",
51
- "@object-ui/plugin-detail": "4.0.6",
52
- "@object-ui/plugin-form": "4.0.6",
53
- "@object-ui/plugin-grid": "4.0.6",
54
- "@object-ui/plugin-kanban": "4.0.6",
55
- "@object-ui/plugin-list": "4.0.6",
56
- "@object-ui/plugin-report": "4.0.6",
57
- "@object-ui/plugin-view": "4.0.6"
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.6.0",
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.5",
64
- "react-dom": "19.2.5",
65
- "react-router-dom": "^7.14.2",
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.10"
68
+ "vite": "^8.0.12"
69
69
  },
70
70
  "keywords": [
71
71
  "objectui",