@object-ui/app-shell 4.0.8 → 4.0.10
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 +58 -0
- package/dist/layout/PageHeader.js +1 -1
- package/dist/views/ObjectView.js +237 -72
- package/package.json +24 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7cb0c37: metadata
|
|
8
|
+
- @object-ui/types@4.0.10
|
|
9
|
+
- @object-ui/core@4.0.10
|
|
10
|
+
- @object-ui/i18n@4.0.10
|
|
11
|
+
- @object-ui/react@4.0.10
|
|
12
|
+
- @object-ui/components@4.0.10
|
|
13
|
+
- @object-ui/fields@4.0.10
|
|
14
|
+
- @object-ui/layout@4.0.10
|
|
15
|
+
- @object-ui/data-objectstack@4.0.10
|
|
16
|
+
- @object-ui/auth@4.0.10
|
|
17
|
+
- @object-ui/permissions@4.0.10
|
|
18
|
+
- @object-ui/plugin-calendar@4.0.10
|
|
19
|
+
- @object-ui/plugin-charts@4.0.10
|
|
20
|
+
- @object-ui/plugin-chatbot@4.0.10
|
|
21
|
+
- @object-ui/plugin-dashboard@4.0.10
|
|
22
|
+
- @object-ui/plugin-designer@4.0.10
|
|
23
|
+
- @object-ui/plugin-detail@4.0.10
|
|
24
|
+
- @object-ui/plugin-form@4.0.10
|
|
25
|
+
- @object-ui/plugin-grid@4.0.10
|
|
26
|
+
- @object-ui/plugin-kanban@4.0.10
|
|
27
|
+
- @object-ui/plugin-list@4.0.10
|
|
28
|
+
- @object-ui/plugin-report@4.0.10
|
|
29
|
+
- @object-ui/plugin-view@4.0.10
|
|
30
|
+
- @object-ui/collaboration@4.0.10
|
|
31
|
+
|
|
32
|
+
## 4.0.9
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- 19c044f: i18n
|
|
37
|
+
- @object-ui/types@4.0.9
|
|
38
|
+
- @object-ui/core@4.0.9
|
|
39
|
+
- @object-ui/i18n@4.0.9
|
|
40
|
+
- @object-ui/react@4.0.9
|
|
41
|
+
- @object-ui/components@4.0.9
|
|
42
|
+
- @object-ui/fields@4.0.9
|
|
43
|
+
- @object-ui/layout@4.0.9
|
|
44
|
+
- @object-ui/data-objectstack@4.0.9
|
|
45
|
+
- @object-ui/auth@4.0.9
|
|
46
|
+
- @object-ui/permissions@4.0.9
|
|
47
|
+
- @object-ui/plugin-calendar@4.0.9
|
|
48
|
+
- @object-ui/plugin-charts@4.0.9
|
|
49
|
+
- @object-ui/plugin-chatbot@4.0.9
|
|
50
|
+
- @object-ui/plugin-dashboard@4.0.9
|
|
51
|
+
- @object-ui/plugin-designer@4.0.9
|
|
52
|
+
- @object-ui/plugin-detail@4.0.9
|
|
53
|
+
- @object-ui/plugin-form@4.0.9
|
|
54
|
+
- @object-ui/plugin-grid@4.0.9
|
|
55
|
+
- @object-ui/plugin-kanban@4.0.9
|
|
56
|
+
- @object-ui/plugin-list@4.0.9
|
|
57
|
+
- @object-ui/plugin-report@4.0.9
|
|
58
|
+
- @object-ui/plugin-view@4.0.9
|
|
59
|
+
- @object-ui/collaboration@4.0.9
|
|
60
|
+
|
|
3
61
|
## 4.0.8
|
|
4
62
|
|
|
5
63
|
### Patch Changes
|
|
@@ -8,6 +8,6 @@ import { cn } from '@object-ui/components';
|
|
|
8
8
|
* the right with consistent gap.
|
|
9
9
|
*/
|
|
10
10
|
export function PageHeader({ title, description, icon, actions, className, 'data-testid': testId, }) {
|
|
11
|
-
return (_jsxs("div", { className: cn('flex justify-between items-center gap-3 py-
|
|
11
|
+
return (_jsxs("div", { className: cn('flex justify-between items-center gap-3 py-1.5 sm:py-2 px-3 sm:px-4 border-b shrink-0 bg-background z-10', className), "data-testid": testId ?? 'page-header', children: [_jsxs("div", { className: "flex items-center gap-2 sm:gap-2.5 min-w-0 flex-1", children: [icon && (_jsx("div", { className: "bg-primary/10 p-1 sm:p-1.5 rounded-md shrink-0 text-primary", children: icon })), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-sm sm:text-base font-semibold tracking-tight text-foreground truncate", children: title }), description && (_jsx("p", { className: "text-xs text-muted-foreground truncate hidden sm:block max-w-md", children: description }))] })] }), actions && (_jsx("div", { className: "flex items-center gap-1.5 sm:gap-2 shrink-0", children: actions }))] }));
|
|
12
12
|
}
|
|
13
13
|
export default PageHeader;
|
package/dist/views/ObjectView.js
CHANGED
|
@@ -47,6 +47,112 @@ const VIEW_TYPE_ICONS = {
|
|
|
47
47
|
chart: BarChart3,
|
|
48
48
|
};
|
|
49
49
|
const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
|
|
50
|
+
/**
|
|
51
|
+
* Translate a NamedListView spec object (shape: `type`, `label`, `columns`,
|
|
52
|
+
* `filter`, `sort`, `kanban`/`chart`/`gantt`/... sub-blocks, `bulkActions`,
|
|
53
|
+
* `rowActions`, `pagination`, ...) into the `sys_view` storage shape that the
|
|
54
|
+
* server's `sys_view` object schema actually exposes (snake_case scalars +
|
|
55
|
+
* `*_json` text columns for complex shapes).
|
|
56
|
+
*
|
|
57
|
+
* The server rejects any other column name (`bulkActions`, `objectName`,
|
|
58
|
+
* `isPinned`, ...) with `table sys_view has no column named …`, and knex
|
|
59
|
+
* flattens raw arrays into positional bindings which mangles the SQL — so
|
|
60
|
+
* every write path that targets `sys_view` MUST go through this helper.
|
|
61
|
+
*/
|
|
62
|
+
function toSysViewPayload(config, objectName, opts = {}) {
|
|
63
|
+
const VIEW_TYPE_KEYS = [
|
|
64
|
+
'kanban', 'calendar', 'timeline', 'gantt',
|
|
65
|
+
'gallery', 'map', 'chart', 'grid',
|
|
66
|
+
];
|
|
67
|
+
// Fold view-type-specific sub-blocks plus any non-storage NamedListView
|
|
68
|
+
// fields into a single `config_json` blob so the round-trip preserves
|
|
69
|
+
// everything the renderer might need (bulkActions, rowActions, pagination,
|
|
70
|
+
// navigation, emptyState, exportOptions, rowHeight, isPinned, isDefault,
|
|
71
|
+
// visibility, sortOrder, …).
|
|
72
|
+
const subConfig = {};
|
|
73
|
+
for (const k of VIEW_TYPE_KEYS) {
|
|
74
|
+
if (config[k] && typeof config[k] === 'object') {
|
|
75
|
+
Object.assign(subConfig, config[k]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const EXTRA_CONFIG_KEYS = [
|
|
79
|
+
'bulkActions', 'rowActions', 'pagination', 'navigation',
|
|
80
|
+
'emptyState', 'exportOptions', 'rowHeight',
|
|
81
|
+
'isPinned', 'isDefault', 'visibility', 'sortOrder',
|
|
82
|
+
'showSort',
|
|
83
|
+
];
|
|
84
|
+
for (const k of EXTRA_CONFIG_KEYS) {
|
|
85
|
+
if (config[k] !== undefined)
|
|
86
|
+
subConfig[k] = config[k];
|
|
87
|
+
}
|
|
88
|
+
const incomingColumns = Array.isArray(config.columns) && config.columns.length > 0
|
|
89
|
+
? config.columns
|
|
90
|
+
: (opts.defaultColumns ?? []);
|
|
91
|
+
const viewType = config.type || 'grid';
|
|
92
|
+
const baseLabel = config.label || config.name || 'Untitled View';
|
|
93
|
+
const slug = baseLabel
|
|
94
|
+
.toLowerCase()
|
|
95
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
96
|
+
.replace(/^_+|_+$/g, '')
|
|
97
|
+
.slice(0, 60) || 'view';
|
|
98
|
+
return {
|
|
99
|
+
name: `${slug}_${Date.now().toString(36)}`,
|
|
100
|
+
label: baseLabel,
|
|
101
|
+
object_name: objectName,
|
|
102
|
+
view_type: viewType,
|
|
103
|
+
columns_json: JSON.stringify(incomingColumns),
|
|
104
|
+
filters_json: config.filter ? JSON.stringify(config.filter) : null,
|
|
105
|
+
sort_json: config.sort ? JSON.stringify(config.sort) : null,
|
|
106
|
+
config_json: Object.keys(subConfig).length > 0
|
|
107
|
+
? JSON.stringify(subConfig)
|
|
108
|
+
: null,
|
|
109
|
+
page_size: config.pageSize ?? 25,
|
|
110
|
+
show_search: config.showSearch !== false,
|
|
111
|
+
show_filters: config.showFilters !== false,
|
|
112
|
+
managed_by: 'user',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Inverse of {@link toSysViewPayload}. Decodes a raw `sys_view` record
|
|
117
|
+
* (snake_case fields + `*_json` text columns) back into the NamedListView
|
|
118
|
+
* spec shape (`type`, `columns`, `filter`, `sort`, plus everything stashed
|
|
119
|
+
* inside `config_json`) so the rest of the UI — ViewTabBar, ListView, the
|
|
120
|
+
* grid renderer — can consume it without caring about the storage layer.
|
|
121
|
+
*
|
|
122
|
+
* Robust to fixtures/mocks that already store the spec shape directly
|
|
123
|
+
* (older tests, the in-memory dev adapter): if `*_json` is missing we fall
|
|
124
|
+
* back to `sv.columns` / `sv.filter` / `sv.sort` as-is.
|
|
125
|
+
*/
|
|
126
|
+
function fromSysViewRecord(sv) {
|
|
127
|
+
const parse = (raw) => {
|
|
128
|
+
if (raw == null)
|
|
129
|
+
return undefined;
|
|
130
|
+
if (typeof raw !== 'string')
|
|
131
|
+
return raw;
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(raw);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const columns = parse(sv.columns_json) ?? sv.columns;
|
|
140
|
+
const filter = parse(sv.filters_json) ?? sv.filter;
|
|
141
|
+
const sort = parse(sv.sort_json) ?? sv.sort;
|
|
142
|
+
const extra = parse(sv.config_json) ?? {};
|
|
143
|
+
return {
|
|
144
|
+
...sv,
|
|
145
|
+
...extra,
|
|
146
|
+
type: sv.view_type ?? sv.type ?? 'grid',
|
|
147
|
+
objectName: sv.object_name ?? sv.objectName,
|
|
148
|
+
columns,
|
|
149
|
+
filter,
|
|
150
|
+
sort,
|
|
151
|
+
showSearch: sv.show_search ?? sv.showSearch,
|
|
152
|
+
showFilters: sv.show_filters ?? sv.showFilters,
|
|
153
|
+
pageSize: sv.page_size ?? sv.pageSize,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
50
156
|
/**
|
|
51
157
|
* DrawerDetailContent — extracted component for NavigationOverlay content.
|
|
52
158
|
* Needs to be a proper component (not a render prop) so it can use hooks
|
|
@@ -289,46 +395,21 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
289
395
|
const incomingColumns = Array.isArray(config.columns) && config.columns.length > 0
|
|
290
396
|
? config.columns
|
|
291
397
|
: defaultColumns;
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
398
|
+
// ADR-0005 overlay path — write the full spec under a unique
|
|
399
|
+
// `name` via the metadata customization API instead of into
|
|
400
|
+
// the physical `sys_view` table (whose columns no longer
|
|
401
|
+
// accommodate the spec shape: arrays, nested objects, etc.).
|
|
402
|
+
const spec = { ...config, columns: incomingColumns };
|
|
403
|
+
if (typeof dataSource?.createView === 'function') {
|
|
404
|
+
const created = await dataSource.createView(objectName, spec);
|
|
405
|
+
createdId = created?.name || config?.name;
|
|
406
|
+
}
|
|
407
|
+
else if (dataSource?.create) {
|
|
408
|
+
// Legacy fallback for adapters that don't expose createView.
|
|
409
|
+
const payload = toSysViewPayload(spec, objectName, { defaultColumns });
|
|
410
|
+
const created = await dataSource.create('sys_view', payload);
|
|
411
|
+
createdId = created?.id ?? created?._id;
|
|
306
412
|
}
|
|
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';
|
|
314
|
-
const payload = {
|
|
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',
|
|
329
|
-
};
|
|
330
|
-
const created = await dataSource.create('sys_view', payload);
|
|
331
|
-
createdId = created?.id ?? created?._id;
|
|
332
413
|
}
|
|
333
414
|
setShowViewConfigPanel(false);
|
|
334
415
|
setViewConfigPanelMode('edit');
|
|
@@ -378,13 +459,47 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
378
459
|
const [savedViews, setSavedViews] = useState([]);
|
|
379
460
|
useEffect(() => {
|
|
380
461
|
let cancelled = false;
|
|
381
|
-
if (!
|
|
462
|
+
if (!objectName) {
|
|
463
|
+
setSavedViews([]);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
// ADR-0005: use the metadata overlay API instead of writing to
|
|
467
|
+
// the physical `sys_view` table (which has incompatible columns).
|
|
468
|
+
// Falls back to the legacy `find('sys_view', ...)` path only when
|
|
469
|
+
// the adapter doesn't expose `listViews` (e.g. test mocks).
|
|
470
|
+
if (typeof dataSource?.listViews === 'function') {
|
|
471
|
+
dataSource.listViews(objectName)
|
|
472
|
+
.then((rows) => {
|
|
473
|
+
if (cancelled)
|
|
474
|
+
return;
|
|
475
|
+
// Normalize: ensure each view has an `id` for ViewTabBar
|
|
476
|
+
// (which is name-keyed downstream). Stamp `objectName`
|
|
477
|
+
// so the defensive filter in handlers still works.
|
|
478
|
+
const normalized = (rows || []).map((sv) => ({
|
|
479
|
+
...sv,
|
|
480
|
+
// Overlay rows are keyed by `name`. Prefer that as the
|
|
481
|
+
// tab id so a duplicate's `id` field (which may have
|
|
482
|
+
// been copied verbatim from the source artifact) does
|
|
483
|
+
// not collide with the source's view id during dedup.
|
|
484
|
+
id: sv.name || sv.id,
|
|
485
|
+
objectName: sv.objectName || sv.object || objectName,
|
|
486
|
+
}));
|
|
487
|
+
setSavedViews(normalized);
|
|
488
|
+
})
|
|
489
|
+
.catch((err) => {
|
|
490
|
+
console.error('[ObjectView] Failed to load overlay views:', err);
|
|
491
|
+
if (!cancelled)
|
|
492
|
+
setSavedViews([]);
|
|
493
|
+
});
|
|
494
|
+
return () => { cancelled = true; };
|
|
495
|
+
}
|
|
496
|
+
if (!dataSource?.find) {
|
|
382
497
|
setSavedViews([]);
|
|
383
498
|
return;
|
|
384
499
|
}
|
|
385
500
|
dataSource
|
|
386
501
|
.find('sys_view', {
|
|
387
|
-
$filter: ['
|
|
502
|
+
$filter: ['object_name', '=', objectName],
|
|
388
503
|
$orderby: [{ field: 'created_at', order: 'asc' }],
|
|
389
504
|
$top: 200,
|
|
390
505
|
})
|
|
@@ -400,8 +515,12 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
400
515
|
// Defensive client-side filter: only keep rows that look like
|
|
401
516
|
// sys_view records for *this* object. Adapters that don't
|
|
402
517
|
// honour $filter (or test mocks that ignore it) won't pollute
|
|
403
|
-
// the view list with arbitrary records.
|
|
404
|
-
|
|
518
|
+
// the view list with arbitrary records. Match either casing —
|
|
519
|
+
// the storage column is `object_name`, but older mock fixtures
|
|
520
|
+
// and tests may still emit `objectName`.
|
|
521
|
+
const filtered = rows
|
|
522
|
+
.filter(r => r && (r.object_name === objectName || r.objectName === objectName))
|
|
523
|
+
.map(fromSysViewRecord);
|
|
405
524
|
setSavedViews(filtered);
|
|
406
525
|
})
|
|
407
526
|
.catch((err) => {
|
|
@@ -666,21 +785,25 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
666
785
|
return savedViews.some((sv) => (sv.id || sv._id) === vid);
|
|
667
786
|
}, [savedViews]);
|
|
668
787
|
const handleRenameView = useCallback(async (vid, newName) => {
|
|
669
|
-
if (!dataSource?.update)
|
|
670
|
-
return;
|
|
671
788
|
if (!isSavedView(vid)) {
|
|
672
789
|
toast.error(t('console.objectView.cannotEditMetaView') || 'Built-in views cannot be renamed.');
|
|
673
790
|
return;
|
|
674
791
|
}
|
|
675
792
|
try {
|
|
676
|
-
|
|
793
|
+
// ADR-0005 overlay path — `vid` is the view's `name` field
|
|
794
|
+
if (typeof dataSource?.updateView === 'function') {
|
|
795
|
+
await dataSource.updateView(objectName, vid, { label: newName });
|
|
796
|
+
}
|
|
797
|
+
else if (dataSource?.update) {
|
|
798
|
+
await dataSource.update('sys_view', vid, { label: newName });
|
|
799
|
+
}
|
|
677
800
|
setRefreshKey(k => k + 1);
|
|
678
801
|
}
|
|
679
802
|
catch (err) {
|
|
680
803
|
console.error('[ViewTabBar] Failed to rename view:', err);
|
|
681
804
|
toast.error(t('objectViewActions.renameFailed'));
|
|
682
805
|
}
|
|
683
|
-
}, [dataSource, isSavedView, t]);
|
|
806
|
+
}, [dataSource, objectName, isSavedView, t]);
|
|
684
807
|
// Promise-based confirm/param dialogs — declared early so destructive
|
|
685
808
|
// handlers (delete, etc.) can `await confirmHandler(...)` for a proper
|
|
686
809
|
// Airtable-style confirmation flow.
|
|
@@ -697,7 +820,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
697
820
|
});
|
|
698
821
|
}, []);
|
|
699
822
|
const handleDeleteView = useCallback(async (vid) => {
|
|
700
|
-
if (!dataSource
|
|
823
|
+
if (!dataSource)
|
|
701
824
|
return;
|
|
702
825
|
if (!isSavedView(vid)) {
|
|
703
826
|
toast.error(t('console.objectView.cannotDeleteMetaView') || 'Built-in views cannot be deleted.');
|
|
@@ -714,7 +837,12 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
714
837
|
if (!confirmed)
|
|
715
838
|
return;
|
|
716
839
|
try {
|
|
717
|
-
|
|
840
|
+
if (typeof dataSource?.deleteView === 'function') {
|
|
841
|
+
await dataSource.deleteView(objectName, vid);
|
|
842
|
+
}
|
|
843
|
+
else if (dataSource?.delete) {
|
|
844
|
+
await dataSource.delete('sys_view', vid);
|
|
845
|
+
}
|
|
718
846
|
// If we deleted the active view, fall back to the first remaining view.
|
|
719
847
|
if (vid === activeViewId) {
|
|
720
848
|
const fallback = views.find((v) => v.id !== vid);
|
|
@@ -729,30 +857,50 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
729
857
|
}
|
|
730
858
|
}, [dataSource, isSavedView, activeViewId, views, viewId, navigate, t, confirmHandler]);
|
|
731
859
|
const handleDuplicateView = useCallback(async (vid) => {
|
|
732
|
-
if (!dataSource
|
|
860
|
+
if (!dataSource)
|
|
733
861
|
return;
|
|
734
862
|
const source = views.find((v) => v.id === vid);
|
|
735
863
|
if (!source)
|
|
736
864
|
return;
|
|
737
865
|
try {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
866
|
+
// ADR-0005 overlay path — store the full NamedListView spec
|
|
867
|
+
// shape directly under a unique `name`. No legacy sys_view
|
|
868
|
+
// column-flattening required.
|
|
869
|
+
const baseName = String(source.name || vid).toLowerCase()
|
|
870
|
+
.replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '') || 'view';
|
|
871
|
+
const newName = `${baseName}_copy_${Date.now().toString(36)}`;
|
|
872
|
+
// Drop `id` so the overlay row is keyed purely by `name`; copying
|
|
873
|
+
// the source's `id` would collide with the source view in the
|
|
874
|
+
// tab-bar dedup logic and silently shadow it.
|
|
875
|
+
const { id: _omitId, ...rest } = source;
|
|
876
|
+
const spec = {
|
|
741
877
|
...rest,
|
|
742
|
-
|
|
743
|
-
id: newId,
|
|
878
|
+
name: newName,
|
|
744
879
|
label: `${source.label || vid} (Copy)`,
|
|
745
880
|
isDefault: false,
|
|
746
881
|
isPinned: false,
|
|
882
|
+
data: source.data || { provider: 'object', object: objectName },
|
|
883
|
+
object: source.object || objectName,
|
|
747
884
|
};
|
|
748
|
-
|
|
885
|
+
let newId;
|
|
886
|
+
if (typeof dataSource?.createView === 'function') {
|
|
887
|
+
const created = await dataSource.createView(objectName, spec);
|
|
888
|
+
newId = created?.name || newName;
|
|
889
|
+
}
|
|
890
|
+
else if (dataSource?.create) {
|
|
891
|
+
const payload = toSysViewPayload(spec, objectName);
|
|
892
|
+
const created = await dataSource.create('sys_view', payload);
|
|
893
|
+
newId = created?.id ?? created?._id;
|
|
894
|
+
}
|
|
749
895
|
setRefreshKey(k => k + 1);
|
|
750
896
|
// Auto-activate the duplicate (Airtable parity).
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
897
|
+
if (newId) {
|
|
898
|
+
if (viewId) {
|
|
899
|
+
navigate(`../${newId}`, { relative: 'path' });
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
navigate(`view/${newId}`);
|
|
903
|
+
}
|
|
756
904
|
}
|
|
757
905
|
}
|
|
758
906
|
catch (err) {
|
|
@@ -761,22 +909,27 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
761
909
|
}
|
|
762
910
|
}, [dataSource, views, objectName, navigate, viewId]);
|
|
763
911
|
const handlePinView = useCallback(async (vid, pinned) => {
|
|
764
|
-
if (!dataSource
|
|
912
|
+
if (!dataSource)
|
|
765
913
|
return;
|
|
766
914
|
if (!isSavedView(vid)) {
|
|
767
915
|
toast.error(t('console.objectView.cannotEditMetaView') || 'Built-in views cannot be pinned.');
|
|
768
916
|
return;
|
|
769
917
|
}
|
|
770
918
|
try {
|
|
771
|
-
|
|
919
|
+
if (typeof dataSource?.updateView === 'function') {
|
|
920
|
+
await dataSource.updateView(objectName, vid, { isPinned: pinned });
|
|
921
|
+
}
|
|
922
|
+
else if (dataSource?.update) {
|
|
923
|
+
await dataSource.update('sys_view', vid, { isPinned: pinned });
|
|
924
|
+
}
|
|
772
925
|
setRefreshKey(k => k + 1);
|
|
773
926
|
}
|
|
774
927
|
catch (err) {
|
|
775
928
|
console.error('[ViewTabBar] Failed to pin view:', err);
|
|
776
929
|
}
|
|
777
|
-
}, [dataSource, isSavedView, t]);
|
|
930
|
+
}, [dataSource, objectName, isSavedView, t]);
|
|
778
931
|
const handleSetDefaultView = useCallback(async (vid) => {
|
|
779
|
-
if (!dataSource
|
|
932
|
+
if (!dataSource)
|
|
780
933
|
return;
|
|
781
934
|
if (!isSavedView(vid)) {
|
|
782
935
|
toast.error(t('console.objectView.cannotEditMetaView')
|
|
@@ -785,10 +938,18 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
785
938
|
}
|
|
786
939
|
try {
|
|
787
940
|
// Clear `isDefault` on all other saved views, then set this one.
|
|
941
|
+
const hasOverlay = typeof dataSource?.updateView === 'function';
|
|
788
942
|
const updates = savedViews
|
|
789
943
|
.filter((sv) => (sv.id || sv._id) !== vid && sv.isDefault)
|
|
790
|
-
.map((sv) =>
|
|
791
|
-
|
|
944
|
+
.map((sv) => {
|
|
945
|
+
const id = sv.id || sv._id;
|
|
946
|
+
return hasOverlay
|
|
947
|
+
? dataSource.updateView(objectName, id, { isDefault: false })
|
|
948
|
+
: dataSource.update('sys_view', id, { isDefault: false });
|
|
949
|
+
});
|
|
950
|
+
updates.push(hasOverlay
|
|
951
|
+
? dataSource.updateView(objectName, vid, { isDefault: true })
|
|
952
|
+
: dataSource.update('sys_view', vid, { isDefault: true }));
|
|
792
953
|
await Promise.all(updates);
|
|
793
954
|
setRefreshKey(k => k + 1);
|
|
794
955
|
}
|
|
@@ -796,7 +957,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
796
957
|
console.error('[ViewTabBar] Failed to set default view:', err);
|
|
797
958
|
toast.error('Failed to set default view');
|
|
798
959
|
}
|
|
799
|
-
}, [dataSource, savedViews, isSavedView, t]);
|
|
960
|
+
}, [dataSource, objectName, savedViews, isSavedView, t]);
|
|
800
961
|
const handleReorderViews = useCallback(async (orderedIds) => {
|
|
801
962
|
// Persist order for ALL views (incl. metadata) in localStorage so the
|
|
802
963
|
// UI immediately reflects the new ordering, including reorderings
|
|
@@ -809,11 +970,15 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
809
970
|
catch { /* ignore */ }
|
|
810
971
|
// Best-effort: also persist `sortOrder` on each saved view so other
|
|
811
972
|
// sessions / users can pick up the order from the backend.
|
|
812
|
-
if (dataSource
|
|
973
|
+
if (dataSource) {
|
|
974
|
+
const hasOverlay = typeof dataSource?.updateView === 'function';
|
|
813
975
|
const savedIdSet = new Set(savedViews.map((sv) => sv.id || sv._id));
|
|
814
976
|
const updates = orderedIds
|
|
815
977
|
.filter(id => savedIdSet.has(id))
|
|
816
|
-
.map((id, idx) =>
|
|
978
|
+
.map((id, idx) => hasOverlay
|
|
979
|
+
? dataSource.updateView(objectName, id, { sortOrder: idx })
|
|
980
|
+
: dataSource.update?.('sys_view', id, { sortOrder: idx }))
|
|
981
|
+
.filter(Boolean);
|
|
817
982
|
try {
|
|
818
983
|
await Promise.all(updates);
|
|
819
984
|
}
|
|
@@ -1370,12 +1535,12 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1370
1535
|
showVisibilityGroups: true,
|
|
1371
1536
|
}, 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 }))] }));
|
|
1372
1537
|
})(), _jsxs("div", { className: "flex-1 overflow-hidden relative flex flex-row", children: [navOverlay.mode === 'split' && navOverlay.isOpen ? (_jsx(NavigationOverlay, { ...navOverlay, setIsOpen: (open) => { if (!open)
|
|
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
|
|
1538
|
+
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", children: _jsx("div", { className: "h-full overflow-auto", children: _jsx(PluginObjectView, { schema: objectViewSchema, dataSource: dataSource, views: mergedViews, activeViewId: activeViewId, onViewChange: handleViewChange, onEdit: (record) => onEdit?.(record), onRowClick: (record) => {
|
|
1374
1539
|
navOverlay.handleClick(record);
|
|
1375
1540
|
}, 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) => {
|
|
1376
1541
|
const recordId = (record.id || record._id);
|
|
1377
1542
|
return (_jsx(DrawerDetailContent, { objectDef: objectDef, recordId: recordId, dataSource: dataSource, onEdit: onEdit }));
|
|
1378
|
-
} })) : (_jsx("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: _jsx("div", { className: "flex-1 relative overflow-hidden
|
|
1543
|
+
} })) : (_jsx("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: _jsx("div", { className: "flex-1 relative overflow-hidden", children: _jsx("div", { className: "h-full overflow-auto", children: _jsx(PluginObjectView, { schema: objectViewSchema, dataSource: dataSource, views: mergedViews, activeViewId: activeViewId, onViewChange: handleViewChange, onEdit: (record) => onEdit?.(record), onRowClick: (record) => {
|
|
1379
1544
|
navOverlay.handleClick(record);
|
|
1380
1545
|
}, renderListView: renderListView, onCreateView: handleCreateView, onViewAction: handleViewAction }) }) }) })), _jsx(MetadataPanel, { open: showDebug && isAdmin, sections: [
|
|
1381
1546
|
{ title: 'View Configuration', data: activeView },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/app-shell",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
|
|
@@ -27,34 +27,34 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"lucide-react": "^1.14.0",
|
|
29
29
|
"sonner": "^2.0.7",
|
|
30
|
-
"@object-ui/auth": "4.0.
|
|
31
|
-
"@object-ui/collaboration": "4.0.
|
|
32
|
-
"@object-ui/components": "4.0.
|
|
33
|
-
"@object-ui/core": "4.0.
|
|
34
|
-
"@object-ui/data-objectstack": "4.0.
|
|
35
|
-
"@object-ui/fields": "4.0.
|
|
36
|
-
"@object-ui/i18n": "4.0.
|
|
37
|
-
"@object-ui/layout": "4.0.
|
|
38
|
-
"@object-ui/permissions": "4.0.
|
|
39
|
-
"@object-ui/react": "4.0.
|
|
40
|
-
"@object-ui/types": "4.0.
|
|
30
|
+
"@object-ui/auth": "4.0.10",
|
|
31
|
+
"@object-ui/collaboration": "4.0.10",
|
|
32
|
+
"@object-ui/components": "4.0.10",
|
|
33
|
+
"@object-ui/core": "4.0.10",
|
|
34
|
+
"@object-ui/data-objectstack": "4.0.10",
|
|
35
|
+
"@object-ui/fields": "4.0.10",
|
|
36
|
+
"@object-ui/i18n": "4.0.10",
|
|
37
|
+
"@object-ui/layout": "4.0.10",
|
|
38
|
+
"@object-ui/permissions": "4.0.10",
|
|
39
|
+
"@object-ui/react": "4.0.10",
|
|
40
|
+
"@object-ui/types": "4.0.10"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0",
|
|
44
44
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
45
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
46
|
-
"@object-ui/plugin-calendar": "4.0.
|
|
47
|
-
"@object-ui/plugin-charts": "4.0.
|
|
48
|
-
"@object-ui/plugin-chatbot": "4.0.
|
|
49
|
-
"@object-ui/plugin-dashboard": "4.0.
|
|
50
|
-
"@object-ui/plugin-designer": "4.0.
|
|
51
|
-
"@object-ui/plugin-detail": "4.0.
|
|
52
|
-
"@object-ui/plugin-form": "4.0.
|
|
53
|
-
"@object-ui/plugin-grid": "4.0.
|
|
54
|
-
"@object-ui/plugin-kanban": "4.0.
|
|
55
|
-
"@object-ui/plugin-list": "4.0.
|
|
56
|
-
"@object-ui/plugin-report": "4.0.
|
|
57
|
-
"@object-ui/plugin-view": "4.0.
|
|
46
|
+
"@object-ui/plugin-calendar": "4.0.10",
|
|
47
|
+
"@object-ui/plugin-charts": "4.0.10",
|
|
48
|
+
"@object-ui/plugin-chatbot": "4.0.10",
|
|
49
|
+
"@object-ui/plugin-dashboard": "4.0.10",
|
|
50
|
+
"@object-ui/plugin-designer": "4.0.10",
|
|
51
|
+
"@object-ui/plugin-detail": "4.0.10",
|
|
52
|
+
"@object-ui/plugin-form": "4.0.10",
|
|
53
|
+
"@object-ui/plugin-grid": "4.0.10",
|
|
54
|
+
"@object-ui/plugin-kanban": "4.0.10",
|
|
55
|
+
"@object-ui/plugin-list": "4.0.10",
|
|
56
|
+
"@object-ui/plugin-report": "4.0.10",
|
|
57
|
+
"@object-ui/plugin-view": "4.0.10"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^25.7.0",
|