@object-ui/app-shell 4.0.7 → 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 +47 -0
- package/dist/console/AppContent.js +4 -2
- package/dist/hooks/useObjectActions.js +26 -2
- package/dist/layout/AppHeader.js +14 -8
- package/dist/layout/AppSidebar.js +2 -2
- 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/views/CreateViewDialog.js +162 -23
- package/dist/views/DashboardView.js +69 -22
- package/dist/views/ObjectView.d.ts +1 -1
- package/dist/views/ObjectView.js +255 -30
- package/dist/views/RecordDetailView.js +90 -8
- package/dist/views/RecordFormPage.js +20 -1
- package/dist/views/ReportView.js +68 -16
- package/dist/views/ViewConfigPanel.js +24 -6
- package/package.json +29 -29
package/dist/views/ObjectView.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Console ObjectView
|
|
4
4
|
*
|
|
@@ -9,7 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
* - useObjectActions for toolbar create button
|
|
10
10
|
* - ListView delegation for non-grid view types (kanban, calendar, chart, etc.)
|
|
11
11
|
*/
|
|
12
|
-
import { useMemo, useState, useCallback, useEffect, lazy, Suspense } from 'react';
|
|
12
|
+
import { useMemo, useState, useCallback, useEffect, useRef, lazy, Suspense } from 'react';
|
|
13
13
|
import { useParams, useSearchParams, useNavigate } 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 })));
|
|
@@ -25,6 +25,7 @@ import { getIcon } from '../utils/getIcon';
|
|
|
25
25
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
26
26
|
import { ViewConfigPanel } from './ViewConfigPanel';
|
|
27
27
|
import { CreateViewDialog } from './CreateViewDialog';
|
|
28
|
+
import { PageHeader } from '../layout/PageHeader';
|
|
28
29
|
import { useObjectActions } from '../hooks/useObjectActions';
|
|
29
30
|
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
30
31
|
import { usePermissions } from '@object-ui/permissions';
|
|
@@ -197,13 +198,13 @@ function DrawerDetailContent({ objectDef, recordId, dataSource, onEdit }) {
|
|
|
197
198
|
},
|
|
198
199
|
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) })] }));
|
|
199
200
|
}
|
|
200
|
-
export function ObjectView({ dataSource, objects, onEdit }) {
|
|
201
|
+
export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
201
202
|
const navigate = useNavigate();
|
|
202
203
|
const { objectName, viewId } = useParams();
|
|
203
204
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
204
205
|
const { showDebug } = useMetadataInspector();
|
|
205
206
|
const { t } = useObjectTranslation();
|
|
206
|
-
const { objectLabel, objectDescription: objectDesc } = useObjectLabel();
|
|
207
|
+
const { objectLabel, objectDescription: objectDesc, viewLabel, actionLabel, actionConfirm, actionSuccess } = useObjectLabel();
|
|
207
208
|
// Inline view config panel state (Airtable-style right sidebar)
|
|
208
209
|
const [showViewConfigPanel, setShowViewConfigPanel] = useState(false);
|
|
209
210
|
const [viewConfigPanelMode, setViewConfigPanelMode] = useState('edit');
|
|
@@ -213,6 +214,35 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
213
214
|
const [manageViewsOpen, setManageViewsOpen] = useState(false);
|
|
214
215
|
// Draft state for view config edits — cached locally, saved on demand
|
|
215
216
|
const [viewDraft, setViewDraft] = useState(null);
|
|
217
|
+
// Per-view debounce timers + latest pending patch payloads. Keyed by
|
|
218
|
+
// viewId so toggles on different views don't clobber each other. We
|
|
219
|
+
// merge incoming patches into a single payload so rapid successive
|
|
220
|
+
// toggles (e.g. resize-drag emitting 60 events/sec) collapse into one
|
|
221
|
+
// network write.
|
|
222
|
+
const persistTimers = useRef({});
|
|
223
|
+
const persistPending = useRef({});
|
|
224
|
+
const persistViewPatch = useCallback((viewIdLocal, baseViewDef, patch) => {
|
|
225
|
+
if (!dataSource?.updateViewConfig || !objectName || !viewIdLocal)
|
|
226
|
+
return;
|
|
227
|
+
// Merge into pending payload — every key present is the latest
|
|
228
|
+
// value the user intended.
|
|
229
|
+
const prev = persistPending.current[viewIdLocal] || {};
|
|
230
|
+
persistPending.current[viewIdLocal] = { ...prev, ...patch };
|
|
231
|
+
const existing = persistTimers.current[viewIdLocal];
|
|
232
|
+
if (existing)
|
|
233
|
+
clearTimeout(existing);
|
|
234
|
+
persistTimers.current[viewIdLocal] = setTimeout(() => {
|
|
235
|
+
const merged = persistPending.current[viewIdLocal] || {};
|
|
236
|
+
delete persistPending.current[viewIdLocal];
|
|
237
|
+
delete persistTimers.current[viewIdLocal];
|
|
238
|
+
Promise.resolve(dataSource.updateViewConfig(objectName, viewIdLocal, {
|
|
239
|
+
...baseViewDef,
|
|
240
|
+
...merged,
|
|
241
|
+
})).catch((err) => {
|
|
242
|
+
console.error('[ObjectView] Failed to persist view config:', err);
|
|
243
|
+
});
|
|
244
|
+
}, 300);
|
|
245
|
+
}, [dataSource, objectName]);
|
|
216
246
|
const handleViewConfigSave = useCallback((draft) => {
|
|
217
247
|
setViewDraft(draft);
|
|
218
248
|
setRefreshKey(k => k + 1);
|
|
@@ -259,10 +289,43 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
259
289
|
const incomingColumns = Array.isArray(config.columns) && config.columns.length > 0
|
|
260
290
|
? config.columns
|
|
261
291
|
: defaultColumns;
|
|
292
|
+
// Translate NamedListView spec shape (type, label, kanban, chart,
|
|
293
|
+
// gantt, etc., columns, filter, sort) into the sys_view storage
|
|
294
|
+
// shape (view_type, label, object_name, *_json columns).
|
|
295
|
+
// Per spec: front-end follows the protocol, the persistence
|
|
296
|
+
// boundary owns the mapping to physical columns.
|
|
297
|
+
const VIEW_TYPE_KEYS = [
|
|
298
|
+
'kanban', 'calendar', 'timeline', 'gantt',
|
|
299
|
+
'gallery', 'map', 'chart', 'grid',
|
|
300
|
+
];
|
|
301
|
+
const subConfig = {};
|
|
302
|
+
for (const k of VIEW_TYPE_KEYS) {
|
|
303
|
+
if (config[k] && typeof config[k] === 'object') {
|
|
304
|
+
Object.assign(subConfig, config[k]);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const viewType = config.type || 'grid';
|
|
308
|
+
const baseLabel = config.label || config.name || 'Untitled View';
|
|
309
|
+
const slug = baseLabel
|
|
310
|
+
.toLowerCase()
|
|
311
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
312
|
+
.replace(/^_+|_+$/g, '')
|
|
313
|
+
.slice(0, 60) || 'view';
|
|
262
314
|
const payload = {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
315
|
+
name: `${slug}_${Date.now().toString(36)}`,
|
|
316
|
+
label: baseLabel,
|
|
317
|
+
object_name: objectName,
|
|
318
|
+
view_type: viewType,
|
|
319
|
+
columns_json: JSON.stringify(incomingColumns),
|
|
320
|
+
filters_json: config.filter ? JSON.stringify(config.filter) : null,
|
|
321
|
+
sort_json: config.sort ? JSON.stringify(config.sort) : null,
|
|
322
|
+
config_json: Object.keys(subConfig).length > 0
|
|
323
|
+
? JSON.stringify(subConfig)
|
|
324
|
+
: null,
|
|
325
|
+
page_size: config.pageSize ?? 25,
|
|
326
|
+
show_search: config.showSearch !== false,
|
|
327
|
+
show_filters: config.showFilters !== false,
|
|
328
|
+
managed_by: 'user',
|
|
266
329
|
};
|
|
267
330
|
const created = await dataSource.create('sys_view', payload);
|
|
268
331
|
createdId = created?.id ?? created?._id;
|
|
@@ -299,6 +362,13 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
299
362
|
}
|
|
300
363
|
// Refresh trigger — bumped after view CRUD or external data mutations.
|
|
301
364
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
365
|
+
// Propagate externally-triggered refreshes (e.g. global ModalForm submit)
|
|
366
|
+
// into our internal refreshKey so list/data effects re-run.
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (externalRefreshKey === undefined || externalRefreshKey === 0)
|
|
369
|
+
return;
|
|
370
|
+
setRefreshKey(k => k + 1);
|
|
371
|
+
}, [externalRefreshKey]);
|
|
302
372
|
// Import wizard open/close state — toolbar entry triggers it.
|
|
303
373
|
const [showImport, setShowImport] = useState(false);
|
|
304
374
|
// ─── User-defined views (sys_view) ──────────────────────────────────
|
|
@@ -341,6 +411,70 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
341
411
|
});
|
|
342
412
|
return () => { cancelled = true; };
|
|
343
413
|
}, [dataSource, objectName, refreshKey]);
|
|
414
|
+
// Persisted per-view config overrides (e.g. density toggle). Saved
|
|
415
|
+
// separately from `objectDef.listViews` (the embedded definition) via
|
|
416
|
+
// `dataSource.updateViewConfig` and read back here so toggle preferences
|
|
417
|
+
// survive a hard reload. Keyed by viewId → partial view config to merge.
|
|
418
|
+
//
|
|
419
|
+
// Use the batch listViewOverrides() when available — fires one HTTP
|
|
420
|
+
// GET per object instead of N (one per defined view), avoiding a flurry
|
|
421
|
+
// of 404s for objects whose views have never been customized. Falls
|
|
422
|
+
// back to per-view getView() for adapters that don't support the batch
|
|
423
|
+
// method.
|
|
424
|
+
const [viewOverrides, setViewOverrides] = useState({});
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
let cancelled = false;
|
|
427
|
+
if (!dataSource || !objectName) {
|
|
428
|
+
setViewOverrides({});
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const definedViews = (objectDef.listViews || objectDef.list_views || {});
|
|
432
|
+
const ids = Object.keys(definedViews);
|
|
433
|
+
if (ids.length === 0) {
|
|
434
|
+
setViewOverrides({});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const loadBatch = async () => {
|
|
438
|
+
if (typeof dataSource.listViewOverrides === 'function') {
|
|
439
|
+
try {
|
|
440
|
+
const all = await dataSource.listViewOverrides(objectName);
|
|
441
|
+
if (all && typeof all === 'object') {
|
|
442
|
+
const map = {};
|
|
443
|
+
for (const id of ids) {
|
|
444
|
+
if (all[id])
|
|
445
|
+
map[id] = all[id];
|
|
446
|
+
}
|
|
447
|
+
return map;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// fall through to per-view fetch
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (typeof dataSource.getView !== 'function')
|
|
455
|
+
return {};
|
|
456
|
+
const entries = await Promise.all(ids.map(async (id) => {
|
|
457
|
+
try {
|
|
458
|
+
const v = await dataSource.getView(objectName, id);
|
|
459
|
+
return [id, v];
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return [id, null];
|
|
463
|
+
}
|
|
464
|
+
}));
|
|
465
|
+
const map = {};
|
|
466
|
+
for (const [id, v] of entries) {
|
|
467
|
+
if (v && typeof v === 'object')
|
|
468
|
+
map[id] = v;
|
|
469
|
+
}
|
|
470
|
+
return map;
|
|
471
|
+
};
|
|
472
|
+
loadBatch().then((map) => {
|
|
473
|
+
if (!cancelled)
|
|
474
|
+
setViewOverrides(map);
|
|
475
|
+
});
|
|
476
|
+
return () => { cancelled = true; };
|
|
477
|
+
}, [dataSource, objectName, objectDef.listViews, objectDef.list_views, refreshKey]);
|
|
344
478
|
// Resolve Views from objectDef.listViews (camelCase per @objectstack/spec)
|
|
345
479
|
const views = useMemo(() => {
|
|
346
480
|
// Default column resolution priority:
|
|
@@ -374,11 +508,18 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
374
508
|
return [];
|
|
375
509
|
};
|
|
376
510
|
const definedViews = objectDef.listViews || objectDef.list_views || {};
|
|
377
|
-
const viewList = Object.entries(definedViews).map(([key, value]) =>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
511
|
+
const viewList = Object.entries(definedViews).map(([key, value]) => {
|
|
512
|
+
const override = viewOverrides[key];
|
|
513
|
+
// Override wins per-key — saved overrides represent user
|
|
514
|
+
// preferences (density, column widths, etc.) that should
|
|
515
|
+
// shadow the embedded definition.
|
|
516
|
+
return {
|
|
517
|
+
id: key,
|
|
518
|
+
...value,
|
|
519
|
+
...(override || {}),
|
|
520
|
+
type: (override?.type) || value.type || 'grid',
|
|
521
|
+
};
|
|
522
|
+
});
|
|
382
523
|
if (viewList.length === 0) {
|
|
383
524
|
viewList.push({
|
|
384
525
|
id: 'all',
|
|
@@ -468,7 +609,7 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
468
609
|
return (aSaved.created_at || '').localeCompare(bSaved.created_at || '');
|
|
469
610
|
});
|
|
470
611
|
return viewList;
|
|
471
|
-
}, [objectDef, savedViews, t]);
|
|
612
|
+
}, [objectDef, savedViews, viewOverrides, t]);
|
|
472
613
|
// Active View State — merge saved draft if available for this view.
|
|
473
614
|
// Resolution priority: URL viewId → ?view= → user-marked default → first.
|
|
474
615
|
const defaultViewId = useMemo(() => {
|
|
@@ -861,6 +1002,24 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
861
1002
|
// Memoize to avoid unstable object identity on every render (stale closure prevention)
|
|
862
1003
|
const detailNavigation = useMemo(() => activeView?.navigation ?? objectDef.navigation ?? { mode: 'page' }, [activeView?.navigation, objectDef.navigation]);
|
|
863
1004
|
const drawerRecordId = searchParams.get('recordId');
|
|
1005
|
+
/**
|
|
1006
|
+
* URL-derived equality filters in the form `?filter[<field>]=<value>`.
|
|
1007
|
+
* Used by related-list "View All" buttons to scope the destination list
|
|
1008
|
+
* to a single parent record. Emitted as ObjectQL triples (`[field, '=', value]`)
|
|
1009
|
+
* which matches the shape consumed by the list view's data fetcher when
|
|
1010
|
+
* merging base filters.
|
|
1011
|
+
*/
|
|
1012
|
+
const urlFilters = useMemo(() => {
|
|
1013
|
+
const out = [];
|
|
1014
|
+
searchParams.forEach((value, key) => {
|
|
1015
|
+
const m = /^filter\[(.+)\]$/.exec(key);
|
|
1016
|
+
if (m && m[1] && value !== '') {
|
|
1017
|
+
out.push([m[1], '=', value]);
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
return out;
|
|
1021
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1022
|
+
}, [searchParams.toString()]);
|
|
864
1023
|
// Memoize onNavigate to prevent stale closure in useNavigationOverlay's handleClick
|
|
865
1024
|
const handleNavOverlayNavigate = useCallback((recordId, action) => {
|
|
866
1025
|
if (action === 'new_window') {
|
|
@@ -940,6 +1099,35 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
940
1099
|
// Propagate appearance/view-config properties for live preview
|
|
941
1100
|
rowHeight: viewDef.rowHeight ?? listSchema.rowHeight,
|
|
942
1101
|
densityMode: viewDef.densityMode ?? listSchema.densityMode,
|
|
1102
|
+
// Hydrate persisted user preferences so they survive reload
|
|
1103
|
+
// (Airtable-style per-view personal config). All four below go
|
|
1104
|
+
// through the same persistViewPatch helper which debounces and
|
|
1105
|
+
// batches concurrent toggles.
|
|
1106
|
+
sort: viewDef.sort ?? listSchema.sort,
|
|
1107
|
+
filter: (() => {
|
|
1108
|
+
const base = viewDef.filter ?? listSchema.filter;
|
|
1109
|
+
if (!urlFilters.length)
|
|
1110
|
+
return base;
|
|
1111
|
+
const baseArr = Array.isArray(base) ? base : [];
|
|
1112
|
+
return [...baseArr, ...urlFilters];
|
|
1113
|
+
})(),
|
|
1114
|
+
hiddenFields: viewDef.hiddenFields ?? listSchema.hiddenFields,
|
|
1115
|
+
columnState: viewDef.columnState ?? listSchema.columnState,
|
|
1116
|
+
onDensityChange: (mode) => {
|
|
1117
|
+
persistViewPatch(viewDef.id, viewDef, { densityMode: mode });
|
|
1118
|
+
},
|
|
1119
|
+
onSortChange: (sort) => {
|
|
1120
|
+
persistViewPatch(viewDef.id, viewDef, { sort });
|
|
1121
|
+
},
|
|
1122
|
+
onFilterChange: (filter) => {
|
|
1123
|
+
persistViewPatch(viewDef.id, viewDef, { filter });
|
|
1124
|
+
},
|
|
1125
|
+
onHiddenFieldsChange: (hidden) => {
|
|
1126
|
+
persistViewPatch(viewDef.id, viewDef, { hiddenFields: hidden });
|
|
1127
|
+
},
|
|
1128
|
+
onColumnStateChange: (state) => {
|
|
1129
|
+
persistViewPatch(viewDef.id, viewDef, { columnState: state });
|
|
1130
|
+
},
|
|
943
1131
|
inlineEdit: viewDef.inlineEdit ?? viewDef.editRecordsInline ?? listSchema.inlineEdit,
|
|
944
1132
|
appearance: viewDef.showDescription != null
|
|
945
1133
|
? { showDescription: viewDef.showDescription }
|
|
@@ -973,7 +1161,6 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
973
1161
|
searchableFields: viewDef.searchableFields ?? listSchema.searchableFields,
|
|
974
1162
|
filterableFields: viewDef.filterableFields ?? listSchema.filterableFields,
|
|
975
1163
|
resizable: viewDef.resizable ?? listSchema.resizable,
|
|
976
|
-
hiddenFields: viewDef.hiddenFields ?? listSchema.hiddenFields,
|
|
977
1164
|
rowActions: viewDef.rowActions ?? listSchema.rowActions,
|
|
978
1165
|
bulkActions: viewDef.bulkActions ?? listSchema.bulkActions,
|
|
979
1166
|
sharing: viewDef.sharing ?? listSchema.sharing,
|
|
@@ -988,7 +1175,13 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
988
1175
|
aria: viewDef.aria ?? listSchema.aria,
|
|
989
1176
|
tabs: listSchema.tabs,
|
|
990
1177
|
// Propagate filter/sort as default filters/sort for data flow
|
|
991
|
-
...(
|
|
1178
|
+
...((() => {
|
|
1179
|
+
const combined = [
|
|
1180
|
+
...(Array.isArray(viewDef.filter) ? viewDef.filter : []),
|
|
1181
|
+
...urlFilters,
|
|
1182
|
+
];
|
|
1183
|
+
return combined.length ? { filters: combined } : {};
|
|
1184
|
+
})()),
|
|
992
1185
|
...(viewDef.sort?.length ? { sort: viewDef.sort } : {}),
|
|
993
1186
|
options: {
|
|
994
1187
|
kanban: {
|
|
@@ -1054,10 +1247,33 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
1054
1247
|
// and triggers onRefresh on success.
|
|
1055
1248
|
actions.deleteRecord(String(record.id));
|
|
1056
1249
|
}
|
|
1250
|
+
}, onBulkDelete: (records) => {
|
|
1251
|
+
const valid = records.filter((r) => r?.id != null);
|
|
1252
|
+
if (valid.length === 0)
|
|
1253
|
+
return;
|
|
1254
|
+
// Route through actions.execute so the shared AlertDialog
|
|
1255
|
+
// confirms once for the whole batch and the existing
|
|
1256
|
+
// delete handler (now batch-aware) handles refresh + toast.
|
|
1257
|
+
actions.execute({
|
|
1258
|
+
type: 'delete',
|
|
1259
|
+
confirmText: t('console.objectView.bulkDeleteConfirm', {
|
|
1260
|
+
count: valid.length,
|
|
1261
|
+
defaultValue: `Delete ${valid.length} selected records? This cannot be undone.`,
|
|
1262
|
+
}),
|
|
1263
|
+
params: { records: valid },
|
|
1264
|
+
});
|
|
1057
1265
|
}, onRowClick: (record) => {
|
|
1058
1266
|
navOverlay.handleClick(record);
|
|
1267
|
+
}, onSortChange: (sort) => {
|
|
1268
|
+
persistViewPatch(viewDef.id, viewDef, { sort });
|
|
1269
|
+
}, onFilterChange: (filter) => {
|
|
1270
|
+
persistViewPatch(viewDef.id, viewDef, { filter });
|
|
1271
|
+
}, onHiddenFieldsChange: (hidden) => {
|
|
1272
|
+
persistViewPatch(viewDef.id, viewDef, { hiddenFields: hidden });
|
|
1273
|
+
}, onColumnStateChange: (state) => {
|
|
1274
|
+
persistViewPatch(viewDef.id, viewDef, { columnState: state });
|
|
1059
1275
|
}, dataSource: ds }, key));
|
|
1060
|
-
}, [activeView, objectDef, objectName, refreshKey, navOverlay, actions]);
|
|
1276
|
+
}, [activeView, objectDef, objectName, refreshKey, navOverlay, actions, persistViewPatch, urlFilters]);
|
|
1061
1277
|
// Memoize the merged views array so PluginObjectView doesn't get a new
|
|
1062
1278
|
// reference on every render (which would trigger unnecessary data refetches).
|
|
1063
1279
|
const mergedViews = useMemo(() => views.map((v) => v.id === activeViewId && viewDraft && viewDraft.id === v.id
|
|
@@ -1094,13 +1310,22 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
1094
1310
|
}
|
|
1095
1311
|
},
|
|
1096
1312
|
}), [objectDef.name, onEdit, activeView?.showSearch, activeView?.showFilters, activeView?.showSort, navigate, viewId, isAdmin]);
|
|
1097
|
-
return (_jsxs(ActionProvider, { context: { objectName: objectDef.name, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1313
|
+
return (_jsxs(ActionProvider, { context: { objectName: objectDef.name, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [_jsx(PageHeader, { title: objectLabel(objectDef), description: objectDef.description ? objectDesc(objectDef) : undefined, icon: (() => { const I = getIcon(objectDef?.icon); return _jsx(I, { className: "h-4 w-4" }); })(), actions: _jsxs(_Fragment, { children: [can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", onClick: actions.create, className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.new') })] })), can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setShowImport(true), className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", title: t('console.objectView.importTitle'), "data-testid": "object-view-import-button", children: [_jsx(Upload, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.import') })] })), objectDef.actions?.some((a) => a.locations?.includes('list_toolbar')) && (_jsx(SchemaRenderer, { schema: {
|
|
1314
|
+
type: 'action:bar',
|
|
1315
|
+
location: 'list_toolbar',
|
|
1316
|
+
actions: (objectDef.actions || []).map((a) => ({
|
|
1317
|
+
...a,
|
|
1318
|
+
label: actionLabel(objectDef.name, a.name, a.label || a.name),
|
|
1319
|
+
...(a.confirmText !== undefined && {
|
|
1320
|
+
confirmText: actionConfirm(objectDef.name, a.name, a.confirmText),
|
|
1321
|
+
}),
|
|
1322
|
+
...(a.successMessage !== undefined && {
|
|
1323
|
+
successMessage: actionSuccess(objectDef.name, a.name, a.successMessage),
|
|
1324
|
+
}),
|
|
1325
|
+
})),
|
|
1326
|
+
size: 'sm',
|
|
1327
|
+
variant: 'outline',
|
|
1328
|
+
} }))] }) }), showImport && (_jsx(Suspense, { fallback: null, children: _jsx(ImportWizard, { open: showImport, onOpenChange: setShowImport, objectName: objectDef.name, objectLabel: objectLabel(objectDef), fields: Object.entries(objectDef.fields || {}).map(([name, def]) => ({
|
|
1104
1329
|
name,
|
|
1105
1330
|
label: def?.label || name,
|
|
1106
1331
|
type: def?.type || 'text',
|
|
@@ -1124,7 +1349,7 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
1124
1349
|
const isSystem = !saved;
|
|
1125
1350
|
return {
|
|
1126
1351
|
id: view.id,
|
|
1127
|
-
label: view.label,
|
|
1352
|
+
label: viewLabel(objectDef.name, view.name || view.id, view.label || view.name || view.id),
|
|
1128
1353
|
type: view.type,
|
|
1129
1354
|
hasActiveFilters: Array.isArray(view.filter) && view.filter.length > 0,
|
|
1130
1355
|
hasActiveSort: Array.isArray(view.sort) && view.sort.length > 0,
|
|
@@ -1145,14 +1370,14 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
1145
1370
|
showVisibilityGroups: true,
|
|
1146
1371
|
}, onAddView: isAdmin ? handleAddView : undefined, onRenameView: isAdmin ? handleRenameView : undefined, onDeleteView: isAdmin ? handleDeleteView : undefined, onDuplicateView: isAdmin ? handleDuplicateView : undefined, onPinView: isAdmin ? handlePinView : undefined, onSetDefaultView: isAdmin ? handleSetDefaultView : undefined, onConfigView: isAdmin ? handleConfigView : undefined, onManageViews: isAdmin ? () => setManageViewsOpen(true) : undefined }), isAdmin && (_jsx(ManageViewsDialog, { open: manageViewsOpen, onOpenChange: setManageViewsOpen, views: viewTabItems, activeViewId: activeViewId, viewTypeIcons: VIEW_TYPE_ICONS, onRename: handleRenameView, onDelete: handleDeleteView, onDuplicate: handleDuplicateView, onSetDefault: handleSetDefaultView, onSetPinned: handlePinView, onReorder: handleReorderViews, onAddView: handleAddView, onConfigView: handleConfigView }))] }));
|
|
1147
1372
|
})(), _jsxs("div", { className: "flex-1 overflow-hidden relative flex flex-row", children: [navOverlay.mode === 'split' && navOverlay.isOpen ? (_jsx(NavigationOverlay, { ...navOverlay, setIsOpen: (open) => { if (!open)
|
|
1148
|
-
handleDrawerClose(); }, title: objectLabel(objectDef), mainContent: _jsxs("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: [_jsx("div", { className: "flex-1 relative overflow-
|
|
1149
|
-
|
|
1150
|
-
|
|
1373
|
+
handleDrawerClose(); }, title: objectLabel(objectDef), mainContent: _jsxs("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: [_jsx("div", { className: "flex-1 relative overflow-hidden p-3 sm:p-4", children: _jsx("div", { className: "h-full overflow-auto rounded-lg border bg-card shadow-xs", children: _jsx(PluginObjectView, { schema: objectViewSchema, dataSource: dataSource, views: mergedViews, activeViewId: activeViewId, onViewChange: handleViewChange, onEdit: (record) => onEdit?.(record), onRowClick: (record) => {
|
|
1374
|
+
navOverlay.handleClick(record);
|
|
1375
|
+
}, renderListView: renderListView, onCreateView: handleCreateView, onViewAction: handleViewAction }) }) }), typeof recordCount === 'number' && (_jsx("div", { "data-testid": "record-count-footer", className: "border-t px-3 sm:px-4 py-1.5 text-xs text-muted-foreground bg-muted/5 shrink-0", children: t('console.objectView.recordCount', { count: recordCount }) }))] }), children: (record) => {
|
|
1151
1376
|
const recordId = (record.id || record._id);
|
|
1152
1377
|
return (_jsx(DrawerDetailContent, { objectDef: objectDef, recordId: recordId, dataSource: dataSource, onEdit: onEdit }));
|
|
1153
|
-
} })) : (_jsx("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: _jsx("div", { className: "flex-1 relative overflow-
|
|
1154
|
-
|
|
1155
|
-
|
|
1378
|
+
} })) : (_jsx("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: _jsx("div", { className: "flex-1 relative overflow-hidden p-3 sm:p-4", children: _jsx("div", { className: "h-full overflow-auto rounded-lg border bg-card shadow-xs", children: _jsx(PluginObjectView, { schema: objectViewSchema, dataSource: dataSource, views: mergedViews, activeViewId: activeViewId, onViewChange: handleViewChange, onEdit: (record) => onEdit?.(record), onRowClick: (record) => {
|
|
1379
|
+
navOverlay.handleClick(record);
|
|
1380
|
+
}, renderListView: renderListView, onCreateView: handleCreateView, onViewAction: handleViewAction }) }) }) })), _jsx(MetadataPanel, { open: showDebug && isAdmin, sections: [
|
|
1156
1381
|
{ title: 'View Configuration', data: activeView },
|
|
1157
1382
|
{ title: 'Object Definition', data: objectDef },
|
|
1158
1383
|
] }), _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)
|
|
@@ -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
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: 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: 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}`;
|
|
@@ -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', {
|