@object-ui/react 15.0.0 → 16.0.0

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,47 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 16.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d3e19ed: Adapt to framework 15.1: (1) ADR-0067 D2 all-or-nothing publishes — `formatPublishFailures` renders a rolled-back batch as ONE banner anchored on the causal item (`batch_aborted` entries are summarized, not listed as parallel errors); PackagesPage says "rolled back because X" instead of "{n} failed"; the AI chat publish toast surfaces the real reason instead of a bare count. Pre-15.1 partial-publish responses keep their per-item rendering. (2) ADR-0076 D12 honest discovery — `DiscoveryServiceStatus` gains `handlerReady` + `degraded`/`stub` statuses, new backward-tolerant `isServiceUsable()` helper (absent fields keep the pre-15.1 default; `stub`/`handlerReady:false` gate off; `degraded` stays usable), consumed by `isAuthEnabled`/`isAiEnabled` and `ConditionalAuthWrapper`.
8
+
9
+ ### Patch Changes
10
+
11
+ - 59d4fa9: fix(detail): show the "Locked for approval" band on request-tracked backends (objectui#2618)
12
+
13
+ The DetailView approval-lock band keyed only off the record's own
14
+ `approval_status` field, so it never rendered on backends that track the lock
15
+ via an open approval request and never materialize that field — even though
16
+ the lock was real (writes rejected with `RECORD_LOCKED`). The record-level
17
+ `InlineEditContext` now carries the host's `locked`/`lockedReason` signal
18
+ (the same dual-source `approvalLocked` that already gates `canEdit` in
19
+ `RecordDetailView`), and the band renders from it while keeping `DetailView`
20
+ DataSource-agnostic. Also backfills the approval-lock strings into the detail
21
+ translation defaults so a bare DetailView shows the label, not the raw i18n key.
22
+
23
+ - 195a651: refactor(spec-bridge): retire the hand-written `ListViewSpec`/`ListColumn` mirrors in the list-view bridge (#2231 follow-up)
24
+
25
+ The SpecBridge's list-view bridge kept a third hand-written copy of the ListView shape
26
+ (after the zod schema and the TS interface unified in the previous #2231 PR). It now
27
+ derives its input type from `@objectstack/spec/ui` (`Partial<ListView>`, spec `ListColumn`),
28
+ so the bridge can no longer drift from the protocol.
29
+
30
+ Behavior fix surfaced by the real types: spec `columns` is `string[] | ListColumn[]`, but
31
+ the old local interface only admitted `ListColumn[]` — a bare field-name column would have
32
+ produced a broken `{ accessorKey: undefined }` mapping. String columns now map to a default
33
+ column (`{ accessorKey: field, header: field }`).
34
+
35
+ - Updated dependencies [210806a]
36
+ - Updated dependencies [b4ef588]
37
+ - Updated dependencies [ca0f5f0]
38
+ - Updated dependencies [5534535]
39
+ - Updated dependencies [9b8f978]
40
+ - @object-ui/types@16.0.0
41
+ - @object-ui/i18n@16.0.0
42
+ - @object-ui/core@16.0.0
43
+ - @object-ui/data-objectstack@16.0.0
44
+
3
45
  ## 15.0.0
4
46
 
5
47
  ### Patch Changes
@@ -31,6 +31,24 @@ export interface InlineEditContextValue {
31
31
  * is a no-op and consumers must not surface the edit affordance.
32
32
  */
33
33
  canEdit: boolean;
34
+ /**
35
+ * Whether this record is *approval-locked* — a pending approval request has
36
+ * the record locked for writes (the backend rejects updates with
37
+ * `RECORD_LOCKED`). This is a DISTINCT signal from `!canEdit`: a record can
38
+ * be non-editable for many reasons (no permission, wrong lifecycle stage),
39
+ * but only an approval lock warrants the "Locked for approval" band + recall
40
+ * affordance. The host computes it (objectui#2618) — typically from the
41
+ * record's `approval_status` field OR an open request in the approvals API —
42
+ * so the band renders from the same signal that gated `canEdit`, keeping the
43
+ * renderer DataSource-agnostic. Defaults to `false`.
44
+ */
45
+ locked: boolean;
46
+ /**
47
+ * Human-readable reason for the approval lock, surfaced as the band's
48
+ * tooltip. Optional — consumers fall back to their own localized default
49
+ * when omitted.
50
+ */
51
+ lockedReason?: string;
34
52
  /**
35
53
  * Draft of user-edited values. Holds ONLY the keys the user actually
36
54
  * changed, so the save path never writes computed / read-only / untouched
@@ -63,6 +81,15 @@ export interface InlineEditProviderProps {
63
81
  * a single source. Defaults to `true`.
64
82
  */
65
83
  canEdit?: boolean;
84
+ /**
85
+ * Whether the record is approval-locked (objectui#2618). Surfaced verbatim
86
+ * on the context so lock-aware consumers (the DetailView "Locked for
87
+ * approval" band) render from the host's signal instead of re-deriving it
88
+ * from a record field the backend may not materialize. Defaults to `false`.
89
+ */
90
+ locked?: boolean;
91
+ /** Optional human-readable lock reason, surfaced as the band tooltip. */
92
+ lockedReason?: string;
66
93
  children: React.ReactNode;
67
94
  }
68
95
  export declare const InlineEditProvider: React.FC<InlineEditProviderProps>;
@@ -24,7 +24,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
24
24
  */
25
25
  import React from 'react';
26
26
  const InlineEditContext = React.createContext(null);
27
- export const InlineEditProvider = ({ canEdit = true, children, }) => {
27
+ export const InlineEditProvider = ({ canEdit = true, locked = false, lockedReason, children, }) => {
28
28
  const [editing, setEditing] = React.useState(false);
29
29
  const [draft, setDraft] = React.useState({});
30
30
  const [autoFocusField, setAutoFocusField] = React.useState(null);
@@ -56,6 +56,8 @@ export const InlineEditProvider = ({ canEdit = true, children, }) => {
56
56
  const value = React.useMemo(() => ({
57
57
  editing,
58
58
  canEdit,
59
+ locked,
60
+ lockedReason,
59
61
  draft,
60
62
  autoFocusField,
61
63
  saving,
@@ -66,7 +68,7 @@ export const InlineEditProvider = ({ canEdit = true, children, }) => {
66
68
  reset: teardown,
67
69
  setSaving,
68
70
  setError,
69
- }), [editing, canEdit, draft, autoFocusField, saving, error, enter, setField, teardown]);
71
+ }), [editing, canEdit, locked, lockedReason, draft, autoFocusField, saving, error, enter, setField, teardown]);
70
72
  return _jsx(InlineEditContext.Provider, { value: value, children: children });
71
73
  };
72
74
  /**
@@ -9,6 +9,33 @@
9
9
  * Discovery service information structure.
10
10
  * Represents server capabilities and service status.
11
11
  */
12
+ /**
13
+ * Per-service availability entry (framework 15.1+, ADR-0076 D12 "honest
14
+ * capabilities"): discovery no longer hardcodes every registered service as
15
+ * `available` — a dev fake reports `stub`, a serving fallback reports
16
+ * `degraded`, and `handlerReady` says whether a real handler backs the route.
17
+ * Older servers omit `handlerReady` and only ever report
18
+ * `available`/`unavailable`, so consumers must treat the new fields as
19
+ * OPT-IN signals (see {@link isServiceUsable}), never require them.
20
+ */
21
+ export interface DiscoveryServiceStatus {
22
+ enabled: boolean;
23
+ status?: 'available' | 'degraded' | 'stub' | 'unavailable';
24
+ handlerReady?: boolean;
25
+ }
26
+ /**
27
+ * The backward-compatible "can I actually use this service?" check
28
+ * (ADR-0076 D12 console slice):
29
+ *
30
+ * - absent entry / absent fields → usable (pre-15.1 servers say nothing —
31
+ * keep their historical default);
32
+ * - `enabled: false` or `handlerReady: false` → not usable;
33
+ * - `status: 'stub'` → not usable (a dev fake must not be treated as the
34
+ * real service — the exact dishonesty D12 removed);
35
+ * - `status: 'degraded'` → USABLE (a fallback that keeps serving; callers
36
+ * may surface a warning but must not turn the feature off).
37
+ */
38
+ export declare function isServiceUsable(svc: DiscoveryServiceStatus | undefined | null): boolean;
12
39
  export interface DiscoveryInfo {
13
40
  /** Server name and version */
14
41
  name?: string;
@@ -27,25 +54,15 @@ export interface DiscoveryInfo {
27
54
  /** Service availability status */
28
55
  services?: {
29
56
  /** Authentication service status */
30
- auth?: {
31
- enabled: boolean;
32
- status?: 'available' | 'unavailable';
57
+ auth?: DiscoveryServiceStatus & {
33
58
  message?: string;
34
59
  };
35
60
  /** Data access service status */
36
- data?: {
37
- enabled: boolean;
38
- status?: 'available' | 'unavailable';
39
- };
61
+ data?: DiscoveryServiceStatus;
40
62
  /** Metadata service status */
41
- metadata?: {
42
- enabled: boolean;
43
- status?: 'available' | 'unavailable';
44
- };
63
+ metadata?: DiscoveryServiceStatus;
45
64
  /** AI service configuration */
46
- ai?: {
47
- enabled: boolean;
48
- status?: 'available' | 'unavailable';
65
+ ai?: DiscoveryServiceStatus & {
49
66
  /** AI service endpoint route (e.g. '/api/v1/ai') */
50
67
  route?: string;
51
68
  };
@@ -89,13 +106,16 @@ export declare function useDiscovery(): {
89
106
  isLoading: boolean;
90
107
  error: Error | null;
91
108
  /**
92
- * Check if authentication is enabled on the server.
93
- * Defaults to true if discovery data is not available.
109
+ * Check if authentication is enabled AND actually backed by a real
110
+ * handler (ADR-0076 D12 — a `stub`/`handlerReady:false` auth service is
111
+ * not treated as real auth). Defaults to true when discovery data is not
112
+ * available (pre-15.1 servers report nothing).
94
113
  */
95
114
  isAuthEnabled: boolean;
96
115
  /**
97
116
  * Check if AI service is enabled and available on the server.
98
- * Defaults to false if discovery data is not available.
117
+ * Defaults to false if discovery data is not available; `degraded`
118
+ * still counts as available (it serves), `stub` never does.
99
119
  */
100
120
  isAiEnabled: boolean;
101
121
  };
@@ -7,6 +7,29 @@
7
7
  */
8
8
  import { useState, useEffect, useContext } from 'react';
9
9
  import { SchemaRendererContext } from '../context/SchemaRendererContext';
10
+ /**
11
+ * The backward-compatible "can I actually use this service?" check
12
+ * (ADR-0076 D12 console slice):
13
+ *
14
+ * - absent entry / absent fields → usable (pre-15.1 servers say nothing —
15
+ * keep their historical default);
16
+ * - `enabled: false` or `handlerReady: false` → not usable;
17
+ * - `status: 'stub'` → not usable (a dev fake must not be treated as the
18
+ * real service — the exact dishonesty D12 removed);
19
+ * - `status: 'degraded'` → USABLE (a fallback that keeps serving; callers
20
+ * may surface a warning but must not turn the feature off).
21
+ */
22
+ export function isServiceUsable(svc) {
23
+ if (!svc)
24
+ return true;
25
+ if (svc.enabled === false)
26
+ return false;
27
+ if (svc.handlerReady === false)
28
+ return false;
29
+ if (svc.status === 'stub' || svc.status === 'unavailable')
30
+ return false;
31
+ return true;
32
+ }
10
33
  /**
11
34
  * Hook to access discovery information from the ObjectStack server.
12
35
  *
@@ -88,15 +111,19 @@ export function useDiscovery() {
88
111
  isLoading,
89
112
  error,
90
113
  /**
91
- * Check if authentication is enabled on the server.
92
- * Defaults to true if discovery data is not available.
114
+ * Check if authentication is enabled AND actually backed by a real
115
+ * handler (ADR-0076 D12 — a `stub`/`handlerReady:false` auth service is
116
+ * not treated as real auth). Defaults to true when discovery data is not
117
+ * available (pre-15.1 servers report nothing).
93
118
  */
94
- isAuthEnabled: discovery?.services?.auth?.enabled ?? true,
119
+ isAuthEnabled: isServiceUsable(discovery?.services?.auth),
95
120
  /**
96
121
  * Check if AI service is enabled and available on the server.
97
- * Defaults to false if discovery data is not available.
122
+ * Defaults to false if discovery data is not available; `degraded`
123
+ * still counts as available (it serves), `stub` never does.
98
124
  */
99
125
  isAiEnabled: discovery?.services?.ai?.enabled === true &&
100
- discovery?.services?.ai?.status === 'available',
126
+ (discovery?.services?.ai?.status === 'available' || discovery?.services?.ai?.status === 'degraded') &&
127
+ discovery?.services?.ai?.handlerReady !== false,
101
128
  };
102
129
  }
@@ -6,79 +6,15 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  import type { BridgeFn } from '../types';
9
- import type { ListViewGalleryConfig, ListViewTimelineConfig } from '@object-ui/types';
10
- interface ListColumn {
11
- field: string;
12
- label?: string;
13
- width?: number;
14
- align?: string;
15
- hidden?: boolean;
16
- sortable?: boolean;
17
- resizable?: boolean;
18
- wrap?: boolean;
19
- type?: string;
20
- pinned?: string;
21
- summary?: any;
22
- link?: any;
23
- action?: any;
24
- }
25
- interface ListViewSpec {
26
- name?: string;
27
- label?: string;
28
- type?: string;
29
- data?: any;
30
- columns?: ListColumn[];
31
- filter?: any;
32
- sort?: any;
33
- searchableFields?: string[];
34
- filterableFields?: string[];
35
- selection?: any;
36
- pagination?: any;
37
- rowHeight?: string;
38
- resizable?: boolean;
39
- striped?: boolean;
40
- bordered?: boolean;
41
- grouping?: any;
42
- rowColor?: any;
43
- navigation?: any;
44
- kanban?: any;
45
- calendar?: any;
46
- gantt?: any;
47
- gallery?: ListViewGalleryConfig;
48
- timeline?: ListViewTimelineConfig;
49
- rowActions?: string[];
50
- bulkActions?: string[];
51
- bulkActionDefs?: any[];
52
- virtualScroll?: boolean;
53
- conditionalFormatting?: Array<{
54
- condition: string;
55
- style: Record<string, string>;
56
- }>;
57
- inlineEdit?: boolean;
58
- exportOptions?: any;
59
- emptyState?: {
60
- title?: string;
61
- message?: string;
62
- icon?: string;
63
- };
64
- userActions?: any;
65
- appearance?: any;
66
- /** UX-only hint: collapse appearance/grouping cluster into a single popover. */
67
- compactToolbar?: boolean;
68
- tabs?: any[];
69
- addRecord?: any;
70
- showRecordCount?: boolean;
71
- allowPrinting?: boolean;
72
- aria?: {
73
- ariaLabel?: string;
74
- ariaDescribedBy?: string;
75
- role?: string;
76
- };
77
- sharing?: any;
78
- hiddenFields?: string[];
79
- fieldOrder?: string[];
80
- description?: string;
81
- }
9
+ import type { ListView } from '@objectstack/spec/ui';
10
+ /**
11
+ * Bridge input: the spec-canonical ListView (#2231 — the former hand-written
12
+ * `ListViewSpec`/`ListColumn` mirrors are retired; the shape now derives from
13
+ * `@objectstack/spec/ui` and can no longer drift). Relaxed to `Partial` because
14
+ * hosts routinely hand the bridge fragmentary view-configs (every field was
15
+ * optional in the old mirror too, including spec-required `columns`).
16
+ */
17
+ type ListViewSpec = Partial<ListView>;
82
18
  /** Transforms a ListView spec into a DataTable SchemaNode */
83
19
  export declare const bridgeListView: BridgeFn<ListViewSpec>;
84
20
  export {};
@@ -6,6 +6,10 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  function mapColumn(col) {
9
+ // Spec-legacy shorthand: a bare field name stands for a default column.
10
+ if (typeof col === 'string') {
11
+ return { accessorKey: col, header: col };
12
+ }
9
13
  const mapped = {
10
14
  accessorKey: col.field,
11
15
  header: col.label ?? col.field,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/react",
3
- "version": "15.0.0",
3
+ "version": "16.0.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "React bindings and SchemaRenderer component for Object UI",
@@ -24,12 +24,12 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@objectstack/spec": "^14.6.0",
27
+ "@objectstack/spec": "^15.1.1",
28
28
  "react-hook-form": "^7.81.0",
29
- "@object-ui/core": "15.0.0",
30
- "@object-ui/data-objectstack": "15.0.0",
31
- "@object-ui/i18n": "15.0.0",
32
- "@object-ui/types": "15.0.0"
29
+ "@object-ui/core": "16.0.0",
30
+ "@object-ui/data-objectstack": "16.0.0",
31
+ "@object-ui/i18n": "16.0.0",
32
+ "@object-ui/types": "16.0.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^18.0.0 || ^19.0.0",