@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 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-2.5 sm:py-3 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-3 min-w-0 flex-1", children: [icon && (_jsx("div", { className: "bg-primary/10 p-1.5 sm:p-2 rounded-md shrink-0 text-primary", children: icon })), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-base sm:text-lg 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 }))] }));
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;
@@ -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
- // 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
- }
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 (!dataSource?.find || !objectName) {
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: ['objectName', '=', objectName],
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
- const filtered = rows.filter(r => r && r.objectName === objectName);
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
- await dataSource.update('sys_view', vid, { label: newName });
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?.delete)
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
- await dataSource.delete('sys_view', vid);
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?.create)
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
- const { id: _omit, created_at, updated_at, ...rest } = source;
739
- const newId = `view_${Date.now()}`;
740
- const payload = {
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
- objectName,
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
- await dataSource.create('sys_view', payload);
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 (viewId) {
752
- navigate(`../${newId}`, { relative: 'path' });
753
- }
754
- else {
755
- navigate(`view/${newId}`);
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?.update)
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
- await dataSource.update('sys_view', vid, { isPinned: pinned });
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?.update)
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) => dataSource.update('sys_view', sv.id || sv._id, { isDefault: false }));
791
- updates.push(dataSource.update('sys_view', vid, { isDefault: true }));
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?.update) {
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) => dataSource.update('sys_view', id, { sortOrder: 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 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) => {
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 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) => {
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.8",
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.8",
31
- "@object-ui/collaboration": "4.0.8",
32
- "@object-ui/components": "4.0.8",
33
- "@object-ui/core": "4.0.8",
34
- "@object-ui/data-objectstack": "4.0.8",
35
- "@object-ui/fields": "4.0.8",
36
- "@object-ui/i18n": "4.0.8",
37
- "@object-ui/layout": "4.0.8",
38
- "@object-ui/permissions": "4.0.8",
39
- "@object-ui/react": "4.0.8",
40
- "@object-ui/types": "4.0.8"
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.8",
47
- "@object-ui/plugin-charts": "4.0.8",
48
- "@object-ui/plugin-chatbot": "4.0.8",
49
- "@object-ui/plugin-dashboard": "4.0.8",
50
- "@object-ui/plugin-designer": "4.0.8",
51
- "@object-ui/plugin-detail": "4.0.8",
52
- "@object-ui/plugin-form": "4.0.8",
53
- "@object-ui/plugin-grid": "4.0.8",
54
- "@object-ui/plugin-kanban": "4.0.8",
55
- "@object-ui/plugin-list": "4.0.8",
56
- "@object-ui/plugin-report": "4.0.8",
57
- "@object-ui/plugin-view": "4.0.8"
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",