@object-ui/react 15.0.0 → 16.1.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 +190 -0
- package/README.md +24 -0
- package/dist/context/InlineEditContext.d.ts +27 -0
- package/dist/context/InlineEditContext.js +4 -2
- package/dist/context/SchemaRendererContext.d.ts +10 -1
- package/dist/context/SchemaRendererContext.js +7 -2
- package/dist/hooks/useDiscovery.d.ts +37 -17
- package/dist/hooks/useDiscovery.js +32 -5
- package/dist/hooks/useETagCache.js +4 -0
- package/dist/hooks/useExpression.d.ts +8 -2
- package/dist/hooks/useExpression.js +9 -1
- package/dist/hooks/useRecordSearch.js +2 -1
- package/dist/hooks/useViewData.js +10 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/spec-bridge/bridges/list-view.d.ts +9 -73
- package/dist/spec-bridge/bridges/list-view.js +4 -0
- package/dist/utils/error-message.d.ts +21 -0
- package/dist/utils/error-message.js +89 -0
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,195 @@
|
|
|
1
1
|
# @object-ui/react
|
|
2
2
|
|
|
3
|
+
## 16.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 803558e: feat(data): thread the host's authenticated fetch into `provider: 'api'` data sources (#2725)
|
|
8
|
+
|
|
9
|
+
`provider: 'api'` view data sources went through a bare `globalThis.fetch`, so
|
|
10
|
+
custom endpoints (gantt composite trees, report aggregates) carried only
|
|
11
|
+
same-origin cookies while every native `/api/v1/*` request carried
|
|
12
|
+
`Authorization: Bearer` — the moment cookie HMAC verification failed (dev
|
|
13
|
+
restart rotating the fallback auth secret, cookie expiry/rotation in prod)
|
|
14
|
+
those views 401'd while the rest of the app kept working.
|
|
15
|
+
|
|
16
|
+
- **`@object-ui/react`** — `SchemaRendererProvider` accepts an optional
|
|
17
|
+
`apiFetch`; nested providers inherit it from their parent so re-wrapped
|
|
18
|
+
subtrees (react pages, preview surfaces) keep the host's authentication.
|
|
19
|
+
`useViewData` defaults the api-provider adapter's fetch to the context
|
|
20
|
+
`apiFetch` (explicit `adapterOptions.fetch` still wins).
|
|
21
|
+
- **`@object-ui/auth`** — `createAuthenticatedFetch` gains a
|
|
22
|
+
`sameOriginOnly` option: cross-origin URLs pass through to the bare fetch
|
|
23
|
+
with no `Authorization` / `X-Tenant-ID` / `Accept-Language`, so metadata-
|
|
24
|
+
supplied third-party URLs never see the platform token.
|
|
25
|
+
- **`@object-ui/app-shell`** — the console wires
|
|
26
|
+
`createAuthenticatedFetch({ sameOriginOnly: true })` (settle-signal wrapped)
|
|
27
|
+
as `apiFetch` on the root `SchemaRendererProvider`.
|
|
28
|
+
- **`@object-ui/plugin-gantt`** — `ObjectGantt` resolves its api-provider
|
|
29
|
+
DataSource with the context `apiFetch`, covering reads and write-backs.
|
|
30
|
+
|
|
31
|
+
Behaviour is unchanged for hosts that don't provide `apiFetch` (bare fetch +
|
|
32
|
+
cookies, as before).
|
|
33
|
+
|
|
34
|
+
- 2e7d7f0: feat(evaluator): route `{ dialect: 'cel' }` component/action predicates to the canonical CEL engine (#2661)
|
|
35
|
+
|
|
36
|
+
Component and action `visible` / `disabled` / `hidden` predicates were evaluated
|
|
37
|
+
by the home-grown JS `ExpressionEvaluator`, while field rules
|
|
38
|
+
(`visibleWhen`/`readonlyWhen`/`requiredWhen`, via `fieldRules.ts`) and row/list
|
|
39
|
+
conditionals (via `evalRowPredicate`) already delegate to the canonical
|
|
40
|
+
`@objectstack/formula` engine. That split meant a `{ dialect: 'cel' }` predicate
|
|
41
|
+
in a renderer/action surface was executed as **JavaScript** — CEL-only forms
|
|
42
|
+
(`x in list`, `has()`, typed `==`, the `today()`/`daysFromNow()` catalog) behaved
|
|
43
|
+
differently from, or faulted against, the server's enforcement.
|
|
44
|
+
|
|
45
|
+
This converges the remaining tier onto the same engine:
|
|
46
|
+
|
|
47
|
+
- **`@object-ui/core`** — `ExpressionEvaluator.evaluateCondition` now detects a
|
|
48
|
+
`{ dialect: 'cel', source }` envelope and evaluates it on `@objectstack/formula`
|
|
49
|
+
(via `evalFieldPredicate`), binding the `record` namespace plus the whole
|
|
50
|
+
context bag as top-level scope (`record.*`, `features.*`, `user.*`, `app.*`).
|
|
51
|
+
Fail-soft to visible/enabled to match the legacy default; `throwOnError`
|
|
52
|
+
callers still fail closed on a _faulting_ predicate (a genuine `false` never
|
|
53
|
+
throws). This fixes every `SchemaRenderer` visibility/disabled read at once.
|
|
54
|
+
- **`@object-ui/react`** — `toPredicateInput` preserves a CEL envelope instead of
|
|
55
|
+
collapsing it to a `${source}` string, and `useCondition` accepts and forwards
|
|
56
|
+
the envelope (keyed on a stable `(dialect, source)` so it doesn't re-evaluate
|
|
57
|
+
each render). Action buttons (`action-icon`/`group`/`bar`/`button`) therefore
|
|
58
|
+
evaluate CEL `visible`/`enabled`/`disabled` on the canonical engine.
|
|
59
|
+
|
|
60
|
+
**Back-compat:** bare strings and `${…}` templates stay on the legacy JS path
|
|
61
|
+
(deprecation window); only an explicit `{ dialect: 'cel' }` envelope is rerouted.
|
|
62
|
+
`{ dialect: 'template' }` is unaffected.
|
|
63
|
+
|
|
64
|
+
Together with the `^15.1.1` alignment (#2662), a renderer CEL predicate now
|
|
65
|
+
reaches the identical verdict as the server — including the framework's
|
|
66
|
+
`dateField == today()` equality fix (objectstack-ai/framework#3205) once it
|
|
67
|
+
lands in a published 15.x. The broader home-grown-vs-canonical divergence
|
|
68
|
+
motivation is #2661.
|
|
69
|
+
|
|
70
|
+
### Patch Changes
|
|
71
|
+
|
|
72
|
+
- 7cf4051: chore(deps): align every `@objectstack/*` dependency to `^16.0.0-rc.0`
|
|
73
|
+
|
|
74
|
+
Bumps `@objectstack/spec` / `client` / `formula` / `lint` from `^15.1.1` to the
|
|
75
|
+
`16.0.0-rc.0` pre-release across the workspace (root + `apps/console` +
|
|
76
|
+
`apps/site` + all consuming packages). ObjectUI's own packages are already on
|
|
77
|
+
major 16, so this closes the 15↔16 skew between ObjectUI and the `@objectstack`
|
|
78
|
+
contract libraries (which publish in lockstep with `spec`).
|
|
79
|
+
|
|
80
|
+
This is a dependency alignment, not a behavioral migration: the full workspace
|
|
81
|
+
build (43/43) and the `@objectstack`-consuming package test suites
|
|
82
|
+
(`core` / `app-shell` / `data-objectstack` / `plugin-form` / `types`) are green
|
|
83
|
+
against `16.0.0-rc.0` with no source changes required.
|
|
84
|
+
|
|
85
|
+
Practical effect: `@objectstack/client@16.0.0-rc.0` now ships
|
|
86
|
+
`data.batchTransaction` (framework #3271), so `ObjectStackAdapter`'s feature
|
|
87
|
+
detect (`typeof client.data.batchTransaction === 'function'`) routes
|
|
88
|
+
master-detail cross-object saves through the typed SDK method instead of the
|
|
89
|
+
raw `fetch('/api/v1/batch')` fallback — realizing the "verify SDK path" half of
|
|
90
|
+
#2694. The raw-fetch branch stays as a defensive fallback (removal tracked in
|
|
91
|
+
#2694).
|
|
92
|
+
|
|
93
|
+
- 549c67d: chore(lint): clear the mechanical baseline lint errors so these packages' lint gates protect them again
|
|
94
|
+
|
|
95
|
+
Extends the fields/core cleanup from #2709 (objectui#2713). These eight package
|
|
96
|
+
lints were red at baseline on `main`, so their per-package `lint` gate could not
|
|
97
|
+
catch new violations of the same class. Cleared every **error** (no behavior
|
|
98
|
+
change; warnings are out of scope):
|
|
99
|
+
|
|
100
|
+
- **`no-useless-catch`** (`data-objectstack`) — unwrapped five try/catch blocks
|
|
101
|
+
whose `catch` only re-threw; errors still propagate identically.
|
|
102
|
+
- **`preserve-caught-error`** (`cli`, `data-objectstack`, `react`) — the caught
|
|
103
|
+
error's message is inlined into the thrown `Error`; a scoped disable with a
|
|
104
|
+
justifying comment carries each one, because these packages target ES2020
|
|
105
|
+
whose lib types the 1-arg `Error` constructor only (so `{ cause }` won't
|
|
106
|
+
compile) — same reasoning as the core case in #2709.
|
|
107
|
+
- **`prefer-const`** (`plugin-calendar`, `plugin-map`) — `let`→`const` for
|
|
108
|
+
never-reassigned bindings.
|
|
109
|
+
- **`no-empty-object-type`** (`plugin-designer`) — empty extend-only interfaces
|
|
110
|
+
→ equivalent `type` aliases.
|
|
111
|
+
- **`no-useless-assignment`** (`react`) — dropped a dead initializer that both
|
|
112
|
+
branches overwrite before it is read.
|
|
113
|
+
- **`no-require-imports`** (`plugin-calendar`, `plugin-timeline` tests) —
|
|
114
|
+
hoisted `vi.mock` factories now use an `async` factory with
|
|
115
|
+
`await import('react')` instead of `require('react')`.
|
|
116
|
+
- **stale `eslint-disable` directive** (`plugin-markdown`) — removed a
|
|
117
|
+
`react/no-danger` disable whose plugin is not loaded in the flat config (an
|
|
118
|
+
unknown-rule reference that ESLint v10 reports as an error); the rationale is
|
|
119
|
+
kept as a plain comment.
|
|
120
|
+
|
|
121
|
+
- Updated dependencies [0318118]
|
|
122
|
+
- Updated dependencies [1c8935a]
|
|
123
|
+
- Updated dependencies [af1b0db]
|
|
124
|
+
- Updated dependencies [8b8b744]
|
|
125
|
+
- Updated dependencies [7cf4051]
|
|
126
|
+
- Updated dependencies [8c1e415]
|
|
127
|
+
- Updated dependencies [0ea5036]
|
|
128
|
+
- Updated dependencies [2e7d7f0]
|
|
129
|
+
- Updated dependencies [94d4876]
|
|
130
|
+
- Updated dependencies [1100a8b]
|
|
131
|
+
- Updated dependencies [7abe4cd]
|
|
132
|
+
- Updated dependencies [549c67d]
|
|
133
|
+
- Updated dependencies [ebe6494]
|
|
134
|
+
- Updated dependencies [2b17339]
|
|
135
|
+
- Updated dependencies [31b77d4]
|
|
136
|
+
- Updated dependencies [6d4fbe6]
|
|
137
|
+
- Updated dependencies [0a3710b]
|
|
138
|
+
- Updated dependencies [f80aaf2]
|
|
139
|
+
- Updated dependencies [62b9ab5]
|
|
140
|
+
- Updated dependencies [1629313]
|
|
141
|
+
- Updated dependencies [29c6040]
|
|
142
|
+
- Updated dependencies [faebac3]
|
|
143
|
+
- Updated dependencies [2331ac9]
|
|
144
|
+
- Updated dependencies [199fa83]
|
|
145
|
+
- Updated dependencies [eee4ded]
|
|
146
|
+
- @object-ui/i18n@16.1.0
|
|
147
|
+
- @object-ui/core@16.1.0
|
|
148
|
+
- @object-ui/data-objectstack@16.1.0
|
|
149
|
+
- @object-ui/types@16.1.0
|
|
150
|
+
|
|
151
|
+
## 16.0.0
|
|
152
|
+
|
|
153
|
+
### Minor Changes
|
|
154
|
+
|
|
155
|
+
- 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`.
|
|
156
|
+
|
|
157
|
+
### Patch Changes
|
|
158
|
+
|
|
159
|
+
- 59d4fa9: fix(detail): show the "Locked for approval" band on request-tracked backends (objectui#2618)
|
|
160
|
+
|
|
161
|
+
The DetailView approval-lock band keyed only off the record's own
|
|
162
|
+
`approval_status` field, so it never rendered on backends that track the lock
|
|
163
|
+
via an open approval request and never materialize that field — even though
|
|
164
|
+
the lock was real (writes rejected with `RECORD_LOCKED`). The record-level
|
|
165
|
+
`InlineEditContext` now carries the host's `locked`/`lockedReason` signal
|
|
166
|
+
(the same dual-source `approvalLocked` that already gates `canEdit` in
|
|
167
|
+
`RecordDetailView`), and the band renders from it while keeping `DetailView`
|
|
168
|
+
DataSource-agnostic. Also backfills the approval-lock strings into the detail
|
|
169
|
+
translation defaults so a bare DetailView shows the label, not the raw i18n key.
|
|
170
|
+
|
|
171
|
+
- 195a651: refactor(spec-bridge): retire the hand-written `ListViewSpec`/`ListColumn` mirrors in the list-view bridge (#2231 follow-up)
|
|
172
|
+
|
|
173
|
+
The SpecBridge's list-view bridge kept a third hand-written copy of the ListView shape
|
|
174
|
+
(after the zod schema and the TS interface unified in the previous #2231 PR). It now
|
|
175
|
+
derives its input type from `@objectstack/spec/ui` (`Partial<ListView>`, spec `ListColumn`),
|
|
176
|
+
so the bridge can no longer drift from the protocol.
|
|
177
|
+
|
|
178
|
+
Behavior fix surfaced by the real types: spec `columns` is `string[] | ListColumn[]`, but
|
|
179
|
+
the old local interface only admitted `ListColumn[]` — a bare field-name column would have
|
|
180
|
+
produced a broken `{ accessorKey: undefined }` mapping. String columns now map to a default
|
|
181
|
+
column (`{ accessorKey: field, header: field }`).
|
|
182
|
+
|
|
183
|
+
- Updated dependencies [210806a]
|
|
184
|
+
- Updated dependencies [b4ef588]
|
|
185
|
+
- Updated dependencies [ca0f5f0]
|
|
186
|
+
- Updated dependencies [5534535]
|
|
187
|
+
- Updated dependencies [9b8f978]
|
|
188
|
+
- @object-ui/types@16.0.0
|
|
189
|
+
- @object-ui/i18n@16.0.0
|
|
190
|
+
- @object-ui/core@16.0.0
|
|
191
|
+
- @object-ui/data-objectstack@16.0.0
|
|
192
|
+
|
|
3
193
|
## 15.0.0
|
|
4
194
|
|
|
5
195
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -81,6 +81,30 @@ function App() {
|
|
|
81
81
|
}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
### SchemaRendererProvider
|
|
85
|
+
|
|
86
|
+
Injects the host's data source (and optional capabilities) into every renderer
|
|
87
|
+
below it:
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { SchemaRendererProvider } from '@object-ui/react'
|
|
91
|
+
|
|
92
|
+
<SchemaRendererProvider
|
|
93
|
+
dataSource={adapter}
|
|
94
|
+
// Optional: host-authenticated fetch used by `provider: 'api'` view data
|
|
95
|
+
// sources, so custom endpoints carry the same credentials (Authorization,
|
|
96
|
+
// tenant, locale headers) as the native data channel. When omitted,
|
|
97
|
+
// ApiDataSource falls back to the bare global fetch (cookies only).
|
|
98
|
+
apiFetch={authenticatedFetch}
|
|
99
|
+
>
|
|
100
|
+
<SchemaRenderer schema={schema} />
|
|
101
|
+
</SchemaRendererProvider>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Nested providers inherit `apiFetch` from their parent when they don't supply
|
|
105
|
+
their own, so re-wrapped subtrees (embedded pages, preview surfaces) keep the
|
|
106
|
+
host's authentication.
|
|
107
|
+
|
|
84
108
|
## Hooks
|
|
85
109
|
|
|
86
110
|
### useSchemaContext
|
|
@@ -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
|
/**
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { DebugFlags } from '@object-ui/core';
|
|
3
|
+
/**
|
|
4
|
+
* Host-provided fetch used for `provider: 'api'` view data sources so custom
|
|
5
|
+
* endpoints carry the same credentials (Authorization, tenant, locale headers)
|
|
6
|
+
* as the native data channel. Optional — when absent, ApiDataSource falls back
|
|
7
|
+
* to the bare global fetch (cookies only).
|
|
8
|
+
*/
|
|
9
|
+
export type ApiFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
3
10
|
interface SchemaRendererContextType {
|
|
4
11
|
dataSource: any;
|
|
5
12
|
debug?: boolean;
|
|
6
13
|
debugFlags?: DebugFlags;
|
|
14
|
+
apiFetch?: ApiFetch;
|
|
7
15
|
}
|
|
8
16
|
declare const SchemaRendererContext: React.Context<SchemaRendererContextType | null>;
|
|
9
17
|
export { SchemaRendererContext };
|
|
10
|
-
export declare const SchemaRendererProvider: ({ children, dataSource, debug, debugFlags, }: {
|
|
18
|
+
export declare const SchemaRendererProvider: ({ children, dataSource, debug, debugFlags, apiFetch, }: {
|
|
11
19
|
children: React.ReactNode;
|
|
12
20
|
dataSource: any;
|
|
13
21
|
debug?: boolean;
|
|
14
22
|
debugFlags?: DebugFlags;
|
|
23
|
+
apiFetch?: ApiFetch;
|
|
15
24
|
}) => React.JSX.Element;
|
|
16
25
|
export declare const useSchemaContext: () => SchemaRendererContextType;
|
|
17
26
|
export declare const useDataScope: (path?: string) => any;
|
|
@@ -2,8 +2,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { createContext, useContext, useMemo } from 'react';
|
|
3
3
|
const SchemaRendererContext = createContext(null);
|
|
4
4
|
export { SchemaRendererContext };
|
|
5
|
-
export const SchemaRendererProvider = ({ children, dataSource, debug, debugFlags, }) => {
|
|
6
|
-
|
|
5
|
+
export const SchemaRendererProvider = ({ children, dataSource, debug, debugFlags, apiFetch, }) => {
|
|
6
|
+
// Nested providers (react-page, studio preview surfaces) re-wrap with their
|
|
7
|
+
// own dataSource but rarely know about the host's authenticated fetch —
|
|
8
|
+
// inherit it from the parent context so provider:'api' auth survives nesting.
|
|
9
|
+
const parent = useContext(SchemaRendererContext);
|
|
10
|
+
const effectiveApiFetch = apiFetch ?? parent?.apiFetch;
|
|
11
|
+
const value = useMemo(() => ({ dataSource, debug, debugFlags, apiFetch: effectiveApiFetch }), [dataSource, debug, debugFlags, effectiveApiFetch]);
|
|
7
12
|
return (_jsx(SchemaRendererContext.Provider, { value: value, children: children }));
|
|
8
13
|
};
|
|
9
14
|
export const useSchemaContext = () => {
|
|
@@ -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
|
|
93
|
-
*
|
|
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
|
|
92
|
-
*
|
|
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
|
|
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
|
}
|
|
@@ -222,6 +222,10 @@ export function useETagCache(userConfig = {}) {
|
|
|
222
222
|
catch (e) {
|
|
223
223
|
if (e instanceof Error && e.message.startsWith('Unsupported'))
|
|
224
224
|
throw e;
|
|
225
|
+
// Any other parse failure collapses to a generic message; we can't
|
|
226
|
+
// attach `e` as the `Error` `cause` because this package targets
|
|
227
|
+
// ES2020, whose lib types the 1-arg `Error` constructor only.
|
|
228
|
+
// eslint-disable-next-line preserve-caught-error
|
|
225
229
|
throw new Error('Invalid URL');
|
|
226
230
|
}
|
|
227
231
|
}
|
|
@@ -40,7 +40,10 @@ export declare function usePredicateScope(): Record<string, any>;
|
|
|
40
40
|
* This is the canonical helper to use in renderers so we never end up with
|
|
41
41
|
* `${[object Object]}` after JS template-literal interpolation.
|
|
42
42
|
*/
|
|
43
|
-
export declare function toPredicateInput(value: unknown): string | boolean |
|
|
43
|
+
export declare function toPredicateInput(value: unknown): string | boolean | {
|
|
44
|
+
dialect: 'cel';
|
|
45
|
+
source: string;
|
|
46
|
+
} | undefined;
|
|
44
47
|
/**
|
|
45
48
|
* Hook for evaluating expressions with dynamic context
|
|
46
49
|
*
|
|
@@ -51,7 +54,10 @@ export declare function toPredicateInput(value: unknown): string | boolean | und
|
|
|
51
54
|
* ```
|
|
52
55
|
*/
|
|
53
56
|
export declare function useExpression(expression: string | boolean | number | null | undefined, context?: Record<string, any>): any;
|
|
54
|
-
export declare function useCondition(condition: string | boolean | undefined
|
|
57
|
+
export declare function useCondition(condition: string | boolean | undefined | {
|
|
58
|
+
dialect?: string;
|
|
59
|
+
source?: string;
|
|
60
|
+
}, context?: Record<string, any>, options?: {
|
|
55
61
|
throwOnError?: boolean;
|
|
56
62
|
label?: string;
|
|
57
63
|
}): boolean;
|
|
@@ -63,7 +63,15 @@ export function toPredicateInput(value) {
|
|
|
63
63
|
return `\${${value}}`;
|
|
64
64
|
if (typeof value === 'object' && typeof value.source === 'string') {
|
|
65
65
|
const src = value.source;
|
|
66
|
-
|
|
66
|
+
if (!src)
|
|
67
|
+
return undefined;
|
|
68
|
+
// #2661 — preserve a CEL-dialect envelope so `useCondition` routes it to the
|
|
69
|
+
// canonical `@objectstack/formula` engine (identical verdict to the server),
|
|
70
|
+
// instead of collapsing it to a `${source}` string on the legacy JS path.
|
|
71
|
+
// Every other dialect (template / unset) keeps the legacy `${…}` behavior.
|
|
72
|
+
if (value.dialect === 'cel')
|
|
73
|
+
return { dialect: 'cel', source: src };
|
|
74
|
+
return `\${${src}}`;
|
|
67
75
|
}
|
|
68
76
|
return undefined;
|
|
69
77
|
}
|
|
@@ -83,7 +83,8 @@ export function useRecordSearch(opts) {
|
|
|
83
83
|
const candidates = useMemo(() => {
|
|
84
84
|
if (!Array.isArray(objects) || objects.length === 0)
|
|
85
85
|
return [];
|
|
86
|
-
|
|
86
|
+
// Assigned in both branches below before it's read; no dead initializer.
|
|
87
|
+
let pool;
|
|
87
88
|
if (Array.isArray(objectNames) && objectNames.length > 0) {
|
|
88
89
|
const byName = new Map();
|
|
89
90
|
for (const obj of objects) {
|
|
@@ -44,18 +44,26 @@ import { SchemaRendererContext } from '../context/SchemaRendererContext';
|
|
|
44
44
|
*/
|
|
45
45
|
export function useViewData(options) {
|
|
46
46
|
const { viewData, resource = '', params, dataSource: explicitDataSource, adapterOptions, skip = false, } = options;
|
|
47
|
-
// Get context DataSource
|
|
47
|
+
// Get context DataSource + host-authenticated fetch
|
|
48
48
|
const context = useContext(SchemaRendererContext);
|
|
49
49
|
const contextDataSource = context?.dataSource ?? null;
|
|
50
|
+
const contextApiFetch = context?.apiFetch;
|
|
50
51
|
// Resolve the DataSource — memoize to prevent re-creation on every render.
|
|
51
52
|
// We key on the viewData provider + config identity.
|
|
52
53
|
const resolvedDataSource = useMemo(() => {
|
|
53
54
|
if (explicitDataSource)
|
|
54
55
|
return explicitDataSource;
|
|
55
|
-
|
|
56
|
+
// Default the 'api' provider's fetch to the host's authenticated fetch
|
|
57
|
+
// (SchemaRendererContext.apiFetch) so custom endpoints carry the same
|
|
58
|
+
// credentials as native requests; explicit adapterOptions still win.
|
|
59
|
+
return resolveDataSource(viewData, contextDataSource, {
|
|
60
|
+
fetch: contextApiFetch,
|
|
61
|
+
...adapterOptions,
|
|
62
|
+
});
|
|
56
63
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
64
|
}, [
|
|
58
65
|
explicitDataSource,
|
|
66
|
+
contextApiFetch,
|
|
59
67
|
viewData?.provider,
|
|
60
68
|
// For 'api' provider, key on read/write URLs
|
|
61
69
|
viewData?.provider === 'api' ? viewData.read?.url : undefined,
|
package/dist/index.d.ts
CHANGED
|
@@ -12,4 +12,5 @@ export * from './LazyPluginLoader';
|
|
|
12
12
|
export * from './spec-bridge';
|
|
13
13
|
export * from './data-invalidation';
|
|
14
14
|
export { resolveI18nLabel } from './utils/i18n';
|
|
15
|
-
export {
|
|
15
|
+
export { extractWriteErrorMessage, isPermissionError } from './utils/error-message';
|
|
16
|
+
export { I18nProvider, useObjectTranslation, useSafeTranslate, useObjectLabel, useSafeFieldLabel, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, type I18nConfig, type I18nProviderProps, type TranslationKeys, type DateFormatOptions, type CurrencyFormatOptions, type NumberFormatOptions, } from '@object-ui/i18n';
|
package/dist/index.js
CHANGED
|
@@ -13,5 +13,8 @@ export * from './spec-bridge';
|
|
|
13
13
|
export * from './data-invalidation';
|
|
14
14
|
// i18n utilities
|
|
15
15
|
export { resolveI18nLabel } from './utils/i18n';
|
|
16
|
+
// Write-error surfacing utilities (shared by drag-write plugins so a failed
|
|
17
|
+
// PATCH — e.g. an RLS 403 — is never silently swallowed).
|
|
18
|
+
export { extractWriteErrorMessage, isPermissionError } from './utils/error-message';
|
|
16
19
|
// Built-in i18n support
|
|
17
|
-
export { I18nProvider, useObjectTranslation, useObjectLabel, useSafeFieldLabel, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from '@object-ui/i18n';
|
|
20
|
+
export { I18nProvider, useObjectTranslation, useSafeTranslate, useObjectLabel, useSafeFieldLabel, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from '@object-ui/i18n';
|
|
@@ -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 {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Pull a clean, human-readable message out of a failed write. Strips noisy
|
|
10
|
+
* backend prefixes (`[Security] `, `PERMISSION_DENIED: `) so it reads well in a
|
|
11
|
+
* toast — mirrors `plugin-detail`'s `cleanError`. Returns `null` when nothing
|
|
12
|
+
* usable can be extracted, so callers fall back to a generic localized string.
|
|
13
|
+
*/
|
|
14
|
+
export declare function extractWriteErrorMessage(err: unknown): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* True when a failed write was rejected for authorization reasons (HTTP 403 /
|
|
17
|
+
* `PERMISSION_DENIED` / `FORBIDDEN` / a row-level-security denial). Lets a
|
|
18
|
+
* caller show a friendly "you don't have permission" toast rather than dumping
|
|
19
|
+
* the raw server text.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isPermissionError(err: unknown): boolean;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Helpers for turning a failed data-source write into user-facing feedback.
|
|
10
|
+
*
|
|
11
|
+
* Different DataSource adapters throw differently shaped errors:
|
|
12
|
+
* - the ObjectStack adapter (`@object-ui/data-objectstack`) decorates the
|
|
13
|
+
* thrown Error with `httpStatus` + a machine-readable `code` and puts the
|
|
14
|
+
* server text on `.message` (sometimes also a parsed body on `.details`);
|
|
15
|
+
* - `ApiDataSource` appends the raw JSON response body as a tail on the
|
|
16
|
+
* message string (`… — {"error":…,"code":…}`).
|
|
17
|
+
*
|
|
18
|
+
* These helpers normalise across both so a caller's `catch` can surface the
|
|
19
|
+
* real reason (e.g. a row-level-security denial) instead of swallowing it.
|
|
20
|
+
*/
|
|
21
|
+
function firstString(...vals) {
|
|
22
|
+
for (const v of vals) {
|
|
23
|
+
if (typeof v === 'string' && v.trim())
|
|
24
|
+
return v;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pull a clean, human-readable message out of a failed write. Strips noisy
|
|
30
|
+
* backend prefixes (`[Security] `, `PERMISSION_DENIED: `) so it reads well in a
|
|
31
|
+
* toast — mirrors `plugin-detail`'s `cleanError`. Returns `null` when nothing
|
|
32
|
+
* usable can be extracted, so callers fall back to a generic localized string.
|
|
33
|
+
*/
|
|
34
|
+
export function extractWriteErrorMessage(err) {
|
|
35
|
+
const e = err;
|
|
36
|
+
// 1) Structured server body, when the adapter attaches one.
|
|
37
|
+
let msg = e && typeof e === 'object'
|
|
38
|
+
? firstString(e.details?.error, e.details?.message, e.error)
|
|
39
|
+
: null;
|
|
40
|
+
// 2) Otherwise the Error message itself. ApiDataSource embeds the raw JSON
|
|
41
|
+
// response body as a tail — parse it out when present.
|
|
42
|
+
if (!msg) {
|
|
43
|
+
const raw = err instanceof Error
|
|
44
|
+
? err.message
|
|
45
|
+
: typeof err === 'string'
|
|
46
|
+
? err
|
|
47
|
+
: typeof e?.message === 'string'
|
|
48
|
+
? e.message
|
|
49
|
+
: '';
|
|
50
|
+
const brace = raw.indexOf('{');
|
|
51
|
+
if (brace >= 0) {
|
|
52
|
+
try {
|
|
53
|
+
const body = JSON.parse(raw.slice(brace));
|
|
54
|
+
msg = firstString(body?.error, body?.message);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* tail wasn't JSON — fall through to the raw message */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!msg && raw.trim())
|
|
61
|
+
msg = raw;
|
|
62
|
+
}
|
|
63
|
+
if (!msg)
|
|
64
|
+
return null;
|
|
65
|
+
const clean = msg
|
|
66
|
+
.replace(/^\[[^\]]+\]\s*/, '')
|
|
67
|
+
.replace(/^[A-Z][A-Z0-9_]+:\s*/, '')
|
|
68
|
+
.trim();
|
|
69
|
+
return clean || null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* True when a failed write was rejected for authorization reasons (HTTP 403 /
|
|
73
|
+
* `PERMISSION_DENIED` / `FORBIDDEN` / a row-level-security denial). Lets a
|
|
74
|
+
* caller show a friendly "you don't have permission" toast rather than dumping
|
|
75
|
+
* the raw server text.
|
|
76
|
+
*/
|
|
77
|
+
export function isPermissionError(err) {
|
|
78
|
+
const e = err;
|
|
79
|
+
if (!e || typeof e !== 'object')
|
|
80
|
+
return false;
|
|
81
|
+
const status = e.httpStatus ?? e.status ?? e.statusCode;
|
|
82
|
+
if (status === 403)
|
|
83
|
+
return true;
|
|
84
|
+
const code = firstString(e.code, e.details?.code) ?? '';
|
|
85
|
+
if (code === 'PERMISSION_DENIED' || code === 'FORBIDDEN')
|
|
86
|
+
return true;
|
|
87
|
+
const text = `${typeof e.message === 'string' ? e.message : ''} ${e.details?.error ?? e.error ?? ''}`;
|
|
88
|
+
return /row-level security|access denied|permission denied|not permitted/i.test(text);
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.1.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": "^
|
|
28
|
-
"react-hook-form": "^7.
|
|
29
|
-
"@object-ui/core": "
|
|
30
|
-
"@object-ui/data-objectstack": "
|
|
31
|
-
"@object-ui/i18n": "
|
|
32
|
-
"@object-ui/types": "
|
|
27
|
+
"@objectstack/spec": "^16.0.0-rc.0",
|
|
28
|
+
"react-hook-form": "^7.82.0",
|
|
29
|
+
"@object-ui/core": "16.1.0",
|
|
30
|
+
"@object-ui/data-objectstack": "16.1.0",
|
|
31
|
+
"@object-ui/i18n": "16.1.0",
|
|
32
|
+
"@object-ui/types": "16.1.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": "^18.0.0 || ^19.0.0",
|