@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
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,9 +9,10 @@ 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
|
+
const ImportWizard = lazy(() => import('@object-ui/plugin-grid').then((m) => ({ default: m.ImportWizard })));
|
|
15
16
|
import { ListView } from '@object-ui/plugin-list';
|
|
16
17
|
import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
|
|
17
18
|
import { ObjectView as PluginObjectView, ViewTabBar, ManageViewsDialog } from '@object-ui/plugin-view';
|
|
@@ -19,11 +20,12 @@ import { ObjectView as PluginObjectView, ViewTabBar, ManageViewsDialog } from '@
|
|
|
19
20
|
// uses ComponentRegistry.registerLazy so heavy plugins stay code-split).
|
|
20
21
|
// Do NOT add eager `import '@object-ui/plugin-*'` side-effect imports here.
|
|
21
22
|
import { Button, Empty, EmptyTitle, EmptyDescription, NavigationOverlay } from '@object-ui/components';
|
|
22
|
-
import { Plus, Table as TableIcon, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
|
|
23
|
+
import { Plus, Upload, Table as TableIcon, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
|
|
23
24
|
import { getIcon } from '../utils/getIcon';
|
|
24
25
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
25
26
|
import { ViewConfigPanel } from './ViewConfigPanel';
|
|
26
27
|
import { CreateViewDialog } from './CreateViewDialog';
|
|
28
|
+
import { PageHeader } from '../layout/PageHeader';
|
|
27
29
|
import { useObjectActions } from '../hooks/useObjectActions';
|
|
28
30
|
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
29
31
|
import { usePermissions } from '@object-ui/permissions';
|
|
@@ -196,13 +198,13 @@ function DrawerDetailContent({ objectDef, recordId, dataSource, onEdit }) {
|
|
|
196
198
|
},
|
|
197
199
|
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) })] }));
|
|
198
200
|
}
|
|
199
|
-
export function ObjectView({ dataSource, objects, onEdit }) {
|
|
201
|
+
export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
200
202
|
const navigate = useNavigate();
|
|
201
203
|
const { objectName, viewId } = useParams();
|
|
202
204
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
203
205
|
const { showDebug } = useMetadataInspector();
|
|
204
206
|
const { t } = useObjectTranslation();
|
|
205
|
-
const { objectLabel, objectDescription: objectDesc } = useObjectLabel();
|
|
207
|
+
const { objectLabel, objectDescription: objectDesc, viewLabel, actionLabel, actionConfirm, actionSuccess } = useObjectLabel();
|
|
206
208
|
// Inline view config panel state (Airtable-style right sidebar)
|
|
207
209
|
const [showViewConfigPanel, setShowViewConfigPanel] = useState(false);
|
|
208
210
|
const [viewConfigPanelMode, setViewConfigPanelMode] = useState('edit');
|
|
@@ -212,6 +214,35 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
212
214
|
const [manageViewsOpen, setManageViewsOpen] = useState(false);
|
|
213
215
|
// Draft state for view config edits — cached locally, saved on demand
|
|
214
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]);
|
|
215
246
|
const handleViewConfigSave = useCallback((draft) => {
|
|
216
247
|
setViewDraft(draft);
|
|
217
248
|
setRefreshKey(k => k + 1);
|
|
@@ -258,10 +289,43 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
258
289
|
const incomingColumns = Array.isArray(config.columns) && config.columns.length > 0
|
|
259
290
|
? config.columns
|
|
260
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';
|
|
261
314
|
const payload = {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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',
|
|
265
329
|
};
|
|
266
330
|
const created = await dataSource.create('sys_view', payload);
|
|
267
331
|
createdId = created?.id ?? created?._id;
|
|
@@ -298,6 +362,15 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
298
362
|
}
|
|
299
363
|
// Refresh trigger — bumped after view CRUD or external data mutations.
|
|
300
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]);
|
|
372
|
+
// Import wizard open/close state — toolbar entry triggers it.
|
|
373
|
+
const [showImport, setShowImport] = useState(false);
|
|
301
374
|
// ─── User-defined views (sys_view) ──────────────────────────────────
|
|
302
375
|
// Saved views created via the ViewConfigPanel ("Add View") are persisted
|
|
303
376
|
// to the `sys_view` object. We fetch them here and merge into `views` so
|
|
@@ -338,6 +411,70 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
338
411
|
});
|
|
339
412
|
return () => { cancelled = true; };
|
|
340
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]);
|
|
341
478
|
// Resolve Views from objectDef.listViews (camelCase per @objectstack/spec)
|
|
342
479
|
const views = useMemo(() => {
|
|
343
480
|
// Default column resolution priority:
|
|
@@ -371,11 +508,18 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
371
508
|
return [];
|
|
372
509
|
};
|
|
373
510
|
const definedViews = objectDef.listViews || objectDef.list_views || {};
|
|
374
|
-
const viewList = Object.entries(definedViews).map(([key, value]) =>
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
});
|
|
379
523
|
if (viewList.length === 0) {
|
|
380
524
|
viewList.push({
|
|
381
525
|
id: 'all',
|
|
@@ -465,7 +609,7 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
465
609
|
return (aSaved.created_at || '').localeCompare(bSaved.created_at || '');
|
|
466
610
|
});
|
|
467
611
|
return viewList;
|
|
468
|
-
}, [objectDef, savedViews, t]);
|
|
612
|
+
}, [objectDef, savedViews, viewOverrides, t]);
|
|
469
613
|
// Active View State — merge saved draft if available for this view.
|
|
470
614
|
// Resolution priority: URL viewId → ?view= → user-marked default → first.
|
|
471
615
|
const defaultViewId = useMemo(() => {
|
|
@@ -534,7 +678,7 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
534
678
|
}
|
|
535
679
|
catch (err) {
|
|
536
680
|
console.error('[ViewTabBar] Failed to rename view:', err);
|
|
537
|
-
toast.error('
|
|
681
|
+
toast.error(t('objectViewActions.renameFailed'));
|
|
538
682
|
}
|
|
539
683
|
}, [dataSource, isSavedView, t]);
|
|
540
684
|
// Promise-based confirm/param dialogs — declared early so destructive
|
|
@@ -581,7 +725,7 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
581
725
|
}
|
|
582
726
|
catch (err) {
|
|
583
727
|
console.error('[ViewTabBar] Failed to delete view:', err);
|
|
584
|
-
toast.error('
|
|
728
|
+
toast.error(t('objectViewActions.deleteFailed'));
|
|
585
729
|
}
|
|
586
730
|
}, [dataSource, isSavedView, activeViewId, views, viewId, navigate, t, confirmHandler]);
|
|
587
731
|
const handleDuplicateView = useCallback(async (vid) => {
|
|
@@ -696,14 +840,6 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
696
840
|
const handleAddView = useCallback(() => {
|
|
697
841
|
setShowCreateViewDialog(true);
|
|
698
842
|
}, []);
|
|
699
|
-
// Action system for toolbar operations — refreshKey moved up (declared earlier).
|
|
700
|
-
const actions = useObjectActions({
|
|
701
|
-
objectName: objectDef.name,
|
|
702
|
-
objectLabel: objectDef.label,
|
|
703
|
-
dataSource,
|
|
704
|
-
onEdit,
|
|
705
|
-
onRefresh: () => setRefreshKey(k => k + 1),
|
|
706
|
-
});
|
|
707
843
|
// ─── ActionProvider handlers for schema-driven toolbar actions ──────
|
|
708
844
|
const currentUser = user
|
|
709
845
|
? { id: user.id, name: user.name, avatar: user.image }
|
|
@@ -714,6 +850,18 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
714
850
|
else
|
|
715
851
|
toast.success(message);
|
|
716
852
|
}, []);
|
|
853
|
+
// Action system for toolbar operations — refreshKey moved up (declared earlier).
|
|
854
|
+
// Wired to confirmHandler/toastHandler so deletes use the Shadcn AlertDialog
|
|
855
|
+
// and Sonner toast instead of native window.confirm.
|
|
856
|
+
const actions = useObjectActions({
|
|
857
|
+
objectName: objectDef.name,
|
|
858
|
+
objectLabel: objectDef.label,
|
|
859
|
+
dataSource,
|
|
860
|
+
onEdit,
|
|
861
|
+
onRefresh: () => setRefreshKey(k => k + 1),
|
|
862
|
+
onConfirm: confirmHandler,
|
|
863
|
+
onToast: toastHandler,
|
|
864
|
+
});
|
|
717
865
|
const navigateHandler = useCallback((url, options) => {
|
|
718
866
|
if (options?.external || options?.newTab) {
|
|
719
867
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
@@ -854,6 +1002,24 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
854
1002
|
// Memoize to avoid unstable object identity on every render (stale closure prevention)
|
|
855
1003
|
const detailNavigation = useMemo(() => activeView?.navigation ?? objectDef.navigation ?? { mode: 'page' }, [activeView?.navigation, objectDef.navigation]);
|
|
856
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()]);
|
|
857
1023
|
// Memoize onNavigate to prevent stale closure in useNavigationOverlay's handleClick
|
|
858
1024
|
const handleNavOverlayNavigate = useCallback((recordId, action) => {
|
|
859
1025
|
if (action === 'new_window') {
|
|
@@ -933,6 +1099,35 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
933
1099
|
// Propagate appearance/view-config properties for live preview
|
|
934
1100
|
rowHeight: viewDef.rowHeight ?? listSchema.rowHeight,
|
|
935
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
|
+
},
|
|
936
1131
|
inlineEdit: viewDef.inlineEdit ?? viewDef.editRecordsInline ?? listSchema.inlineEdit,
|
|
937
1132
|
appearance: viewDef.showDescription != null
|
|
938
1133
|
? { showDescription: viewDef.showDescription }
|
|
@@ -966,7 +1161,6 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
966
1161
|
searchableFields: viewDef.searchableFields ?? listSchema.searchableFields,
|
|
967
1162
|
filterableFields: viewDef.filterableFields ?? listSchema.filterableFields,
|
|
968
1163
|
resizable: viewDef.resizable ?? listSchema.resizable,
|
|
969
|
-
hiddenFields: viewDef.hiddenFields ?? listSchema.hiddenFields,
|
|
970
1164
|
rowActions: viewDef.rowActions ?? listSchema.rowActions,
|
|
971
1165
|
bulkActions: viewDef.bulkActions ?? listSchema.bulkActions,
|
|
972
1166
|
sharing: viewDef.sharing ?? listSchema.sharing,
|
|
@@ -981,7 +1175,13 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
981
1175
|
aria: viewDef.aria ?? listSchema.aria,
|
|
982
1176
|
tabs: listSchema.tabs,
|
|
983
1177
|
// Propagate filter/sort as default filters/sort for data flow
|
|
984
|
-
...(
|
|
1178
|
+
...((() => {
|
|
1179
|
+
const combined = [
|
|
1180
|
+
...(Array.isArray(viewDef.filter) ? viewDef.filter : []),
|
|
1181
|
+
...urlFilters,
|
|
1182
|
+
];
|
|
1183
|
+
return combined.length ? { filters: combined } : {};
|
|
1184
|
+
})()),
|
|
985
1185
|
...(viewDef.sort?.length ? { sort: viewDef.sort } : {}),
|
|
986
1186
|
options: {
|
|
987
1187
|
kanban: {
|
|
@@ -1040,10 +1240,40 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
1040
1240
|
},
|
|
1041
1241
|
},
|
|
1042
1242
|
};
|
|
1043
|
-
return (_jsx(ListView, { schema: fullSchema, className: className, onEdit: editHandler,
|
|
1243
|
+
return (_jsx(ListView, { schema: fullSchema, className: className, onEdit: editHandler, onDelete: (record) => {
|
|
1244
|
+
if (record?.id != null) {
|
|
1245
|
+
// useObjectActions.deleteRecord wraps execute() which
|
|
1246
|
+
// already shows a confirmation dialog + success toast
|
|
1247
|
+
// and triggers onRefresh on success.
|
|
1248
|
+
actions.deleteRecord(String(record.id));
|
|
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
|
+
});
|
|
1265
|
+
}, onRowClick: (record) => {
|
|
1044
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 });
|
|
1045
1275
|
}, dataSource: ds }, key));
|
|
1046
|
-
}, [activeView, objectDef, objectName, refreshKey, navOverlay]);
|
|
1276
|
+
}, [activeView, objectDef, objectName, refreshKey, navOverlay, actions, persistViewPatch, urlFilters]);
|
|
1047
1277
|
// Memoize the merged views array so PluginObjectView doesn't get a new
|
|
1048
1278
|
// reference on every render (which would trigger unnecessary data refetches).
|
|
1049
1279
|
const mergedViews = useMemo(() => views.map((v) => v.id === activeViewId && viewDraft && viewDraft.id === v.id
|
|
@@ -1080,13 +1310,37 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
1080
1310
|
}
|
|
1081
1311
|
},
|
|
1082
1312
|
}), [objectDef.name, onEdit, activeView?.showSearch, activeView?.showFilters, activeView?.showSort, navigate, viewId, isAdmin]);
|
|
1083
|
-
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: [
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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]) => ({
|
|
1329
|
+
name,
|
|
1330
|
+
label: def?.label || name,
|
|
1331
|
+
type: def?.type || 'text',
|
|
1332
|
+
required: !!def?.required,
|
|
1333
|
+
})), dataSource: dataSource, onComplete: (result) => {
|
|
1334
|
+
setRefreshKey(k => k + 1);
|
|
1335
|
+
const ok = result.importedRows;
|
|
1336
|
+
const skip = result.skippedRows;
|
|
1337
|
+
if (skip > 0) {
|
|
1338
|
+
toast.warning(t('console.objectView.importedWithSkipped', { ok, skipped: skip }));
|
|
1339
|
+
}
|
|
1340
|
+
else if (ok > 0) {
|
|
1341
|
+
toast.success(t('console.objectView.importedToast', { count: ok }));
|
|
1342
|
+
}
|
|
1343
|
+
} }) })), views.length >= 1 && (() => {
|
|
1090
1344
|
const viewTabItems = views.map((view) => {
|
|
1091
1345
|
const saved = savedViews.find((sv) => (sv.id || sv._id) === view.id);
|
|
1092
1346
|
// System views (loaded from objectDef.listViews / metadata) are
|
|
@@ -1095,7 +1349,7 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
1095
1349
|
const isSystem = !saved;
|
|
1096
1350
|
return {
|
|
1097
1351
|
id: view.id,
|
|
1098
|
-
label: view.label,
|
|
1352
|
+
label: viewLabel(objectDef.name, view.name || view.id, view.label || view.name || view.id),
|
|
1099
1353
|
type: view.type,
|
|
1100
1354
|
hasActiveFilters: Array.isArray(view.filter) && view.filter.length > 0,
|
|
1101
1355
|
hasActiveSort: Array.isArray(view.sort) && view.sort.length > 0,
|
|
@@ -1116,14 +1370,14 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
1116
1370
|
showVisibilityGroups: true,
|
|
1117
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 }))] }));
|
|
1118
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)
|
|
1119
|
-
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-
|
|
1120
|
-
|
|
1121
|
-
|
|
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) => {
|
|
1122
1376
|
const recordId = (record.id || record._id);
|
|
1123
1377
|
return (_jsx(DrawerDetailContent, { objectDef: objectDef, recordId: recordId, dataSource: dataSource, onEdit: onEdit }));
|
|
1124
|
-
} })) : (_jsx("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: _jsx("div", { className: "flex-1 relative overflow-
|
|
1125
|
-
|
|
1126
|
-
|
|
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: [
|
|
1127
1381
|
{ title: 'View Configuration', data: activeView },
|
|
1128
1382
|
{ title: 'Object Definition', data: objectDef },
|
|
1129
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)
|
package/dist/views/PageView.js
CHANGED
|
@@ -9,11 +9,13 @@ import { useParams, useSearchParams } from 'react-router-dom';
|
|
|
9
9
|
import { SchemaRenderer } from '@object-ui/react';
|
|
10
10
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
11
11
|
import { FileText, Pencil } from 'lucide-react';
|
|
12
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
12
13
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
13
14
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
14
15
|
import { DesignDrawer } from './DesignDrawer';
|
|
15
16
|
const PageCanvasEditor = lazy(() => import('@object-ui/plugin-designer').then((m) => ({ default: m.PageCanvasEditor })));
|
|
16
17
|
export function PageView() {
|
|
18
|
+
const { t } = useObjectTranslation();
|
|
17
19
|
const { pageName } = useParams();
|
|
18
20
|
const [searchParams] = useSearchParams();
|
|
19
21
|
const { showDebug } = useMetadataInspector();
|
|
@@ -35,7 +37,7 @@ export function PageView() {
|
|
|
35
37
|
}
|
|
36
38
|
}, []);
|
|
37
39
|
if (!page) {
|
|
38
|
-
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(FileText, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children:
|
|
40
|
+
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(FileText, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.pageNotFound') }), _jsx(EmptyDescription, { children: t('empty.pageNotFoundDescription', { name: pageName }) })] }) }));
|
|
39
41
|
}
|
|
40
42
|
// Convert URL search params to an object for the page context
|
|
41
43
|
const params = Object.fromEntries(searchParams.entries());
|