@object-ui/react 17.1.0 → 17.2.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,156 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a889e31: A record's approval band now shows the quorum / per-group tally the server already computes.
8
+
9
+ The showcase's `showcase_committee_quorum` node declares `behavior: 'quorum'` with
10
+ `minApprovals: 2` over three approvers, and even ships a pre-rendered
11
+ `"Committee Sign-off (2 of 3)"` label; `showcase_expense_signoff` declares
12
+ `per_group` (会签) with named manager / finance groups. On the business record
13
+ the approval band rendered none of it — the lock badge, the recall button and
14
+ the approve/reject actions were all correct, but a two-of-three committee step
15
+ looked exactly like a one-approver step. An approver could not see whether their
16
+ own click finalized the node or was one of three, which is the single fact a
17
+ quorum node exists to express (objectstack#4478).
18
+
19
+ Nothing was wrong on the wire, and nothing here papers over the server. The
20
+ framework computes `decision_progress` — `{ behavior, got, need, groups? }`,
21
+ derived from the node's own `node_config_json` snapshot, so the count a client
22
+ shows is the count the engine will enforce. **It attaches that block in
23
+ `getRequest` only**: `listRequests` deliberately skips it, because the
24
+ `sys_approval_action` tally it costs is per row and a list read may return
25
+ hundreds. The record header's `useRecordApprovals` reads
26
+ `GET /approvals/requests?object=…&recordId=…` — the list route — so the
27
+ enrichment was never in the payload it had. The hook now follows up with one
28
+ single read for the ONE pending row and folds the result onto it; a failed or
29
+ mismatched follow-up leaves the row exactly as the list sent it, so a display-only
30
+ enrichment can never take the approval panel down and no tally is ever invented.
31
+
32
+ `InlineEditProvider` carries the block through as `approvalProgress`, and the
33
+ DetailView approval band renders it beside the existing badge: a labelled
34
+ `role="progressbar"` with one tick per required approval for `quorum` /
35
+ `unanimous`, and for `per_group` a chip per group marking which have signed
36
+ (`finance 1/1` ✓, `manager 0/1`). Group names come from the flow author's own
37
+ config, so they need no locale strings; the three new label keys are added to all
38
+ ten packs. `first_response` nodes carry no `decision_progress` and are unchanged —
39
+ one decision is the whole step there, and a "1 of 1" bar would be noise.
40
+
41
+ Scored `minor` rather than `patch`: this is new observable rendering plus a new
42
+ public `approvalProgress` prop / `ApprovalProgress` type on `@object-ui/react`,
43
+ not a behavior correction inside an existing surface.
44
+
45
+ - 09d30a4: Stop declaring 18 `@object-ui/auth` / `@object-ui/components` / `@object-ui/react`
46
+ symbols under names `@objectstack/spec` owns (objectui#3159, objectstack#4115
47
+ batch 5).
48
+
49
+ **Breaking for importers of all three packages** — six exported names changed,
50
+ because the spec exports the same name for a _different_ thing:
51
+
52
+ | package | was | now | what the spec's same-named export actually is |
53
+ | :----------- | :--------------------------- | :----------------------- | :----------------------------------------------------------------------------- |
54
+ | `auth` | `AuthSession` | `AuthClientSession` | the SERVER's session record (`{ id, userId, expiresAt: ISO string, token? }`) |
55
+ | `auth` | `AuthProviderConfig` | `AuthProviderOptions` | an OAuth/OIDC provider registration (`{ id, clientId, clientSecret, scope? }`) |
56
+ | `components` | `FilterCondition` | `FilterBuilderCondition` | the recursive ObjectQL predicate AST (`$and`/`$or`/`$not`) |
57
+ | `components` | `Field` | `FieldContainer` | an object FIELD's metadata and its builder namespace |
58
+ | `react` | `ConflictResolutionStrategy` | `ConflictResolution` | the metadata-MERGE policy (`error \| priority \| first-wins \| last-wins`) |
59
+
60
+ The `react` rename is the odd one out: the new name is the **spec's own** name
61
+ for the union that hook always used, so it is a re-export rather than a dialect.
62
+
63
+ Eleven more keep their names and are now **imported or derived from the spec**
64
+ instead of re-declared: `TenancyPosture`, `DelegableScope` (+`DelegableAdminScope`),
65
+ `AuthUser`, `ShareLinkPermission`, `ShareLinkAudience`, `ShareLink`, `SortItem`,
66
+ `OfflineStrategy`, `OfflineCacheConfig`, `OfflineSyncConfig`, `OfflineConfig`,
67
+ `NavigationConfig`.
68
+
69
+ **Three of the copies were losing information, not just duplicating it.**
70
+
71
+ - `AuthUser` never declared the spec's `positions` or `tenantId` — the
72
+ authorization inputs. Its `[key: string]: unknown` index signature meant the
73
+ omission was invisible at every call site _and_ to any structural comparison
74
+ (the objectstack#4075 mechanism). It now `extends` the spec principal, so the
75
+ display-only fields (`image`, `role`, `roles`, `emailVerified`) are the delta
76
+ and the spec's keys arrive on their own.
77
+ - `useNavigationOverlay`'s copy carried the note _"inline … to avoid importing
78
+ from `@object-ui/types` (which may not be a direct dependency of
79
+ `@object-ui/react`)"_. The vocabulary belongs to `@objectstack/spec`, which
80
+ **is** a direct dependency — the same expired "kept local to avoid a
81
+ dependency" comment objectui#3169 found in `@object-ui/app-shell`.
82
+ - `useOffline` and `usePerformance` both opened with _"Types aligned with
83
+ `@objectstack/spec` v2.0.7"_. The installed spec is 17.0.0-rc.1.
84
+
85
+ `ShareLink` derives from the spec row **minus `password_hash`** — omitted rather
86
+ than optional, because it is the credential itself and typing it in a browser
87
+ package is an invitation to render it. `password_protected` (the boolean the UI
88
+ needs in its place) is the one local addition.
89
+
90
+ The config types derive from each schema's **input** side, not `z.infer`.
91
+ `useOffline(config: OfflineConfig = {})` defaults to the empty object, which the
92
+ output type — every `.default()`ed key required — would reject outright.
93
+
94
+ `@objectstack/spec` moves from `devDependencies` to `dependencies` in
95
+ `@object-ui/components`: its public type surface now references the spec.
96
+
97
+ Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
98
+ tracks `@objectstack`, so breaking changes of our own ship as minor with the
99
+ semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
100
+ all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
101
+
102
+ ### Patch Changes
103
+
104
+ - ea96284: `useMetadataItem` no longer spins forever outside a `<MetadataProvider>` — the "graceful fallback" was the thing that made those consumers impossible to mount.
105
+
106
+ `useMetadata()` built its no-provider fallback **inline on every call**, so outside a provider
107
+ every render produced a new `getItem`. `useMetadataItem` lists `getItem` in its effect deps and,
108
+ on the no-name path, called `setState({ item: null, loading: false, error: null })` with a fresh
109
+ object each run. New identity → effect re-runs → new state object → re-render → new identity:
110
+ an unbreakable loop, synchronous enough to hang inside `render()` rather than fail.
111
+
112
+ So the fallback documented as the graceful path for consumers mounted outside a provider —
113
+ "common in unit tests that only need to assert on rendering" — was precisely what made them
114
+ unmountable. `record:alert` and `record:quick_actions` both call `useMetadataItem`
115
+ unconditionally; each pinned a core and grew unbounded (8.6 GB before the first kill) on a
116
+ `render()` that never returned.
117
+
118
+ Two changes, at the cause and one layer in:
119
+
120
+ - The fallback is a frozen module-level singleton, so its identity is stable across renders.
121
+ - The clear-state path bails out when the state is already cleared, instead of installing an
122
+ equal-but-new object. That covers the same loop arriving by another route — any caller whose
123
+ context value is rebuilt per render, which this interface explicitly invites ("hand-rolled
124
+ context values in tests keep working").
125
+
126
+ Found by `apps/console/src/__tests__/record-block-record-reach.test.tsx` (objectui#3149), which
127
+ could not mount either block until this was fixed.
128
+
129
+ - Updated dependencies [4ae0ac4]
130
+ - Updated dependencies [696e3c1]
131
+ - Updated dependencies [bca45cc]
132
+ - Updated dependencies [a889e31]
133
+ - Updated dependencies [4bf612c]
134
+ - Updated dependencies [335041c]
135
+ - Updated dependencies [b414983]
136
+ - Updated dependencies [256f8cc]
137
+ - Updated dependencies [c5ccbd5]
138
+ - Updated dependencies [d9668a7]
139
+ - Updated dependencies [4b470b9]
140
+ - Updated dependencies [cb82705]
141
+ - Updated dependencies [f572849]
142
+ - Updated dependencies [d3584c6]
143
+ - Updated dependencies [a8ad6c0]
144
+ - Updated dependencies [444457c]
145
+ - Updated dependencies [850033c]
146
+ - Updated dependencies [022e4c3]
147
+ - Updated dependencies [009e25d]
148
+ - Updated dependencies [726b89c]
149
+ - @object-ui/types@17.2.0
150
+ - @object-ui/core@17.2.0
151
+ - @object-ui/i18n@17.2.0
152
+ - @object-ui/data-objectstack@17.2.0
153
+
3
154
  ## 17.1.0
4
155
 
5
156
  ### Minor Changes
@@ -7,30 +7,50 @@ export function useAdapter() {
7
7
  return useContext(AdapterCtx);
8
8
  }
9
9
  export const MetadataCtx = createContext(null);
10
+ /** Shared frozen empty list for the fallback below — see {@link NO_METADATA_PROVIDER}. */
11
+ const NO_ITEMS = Object.freeze([]);
12
+ /**
13
+ * The no-provider fallback, as a MODULE-LEVEL SINGLETON.
14
+ *
15
+ * Identity matters here, not just shape. `useMetadata()` used to build this
16
+ * object inline on every call, so outside a `<MetadataProvider>` every render
17
+ * produced a new `getItem` — and `useMetadataItem` lists `getItem` in its
18
+ * effect deps and calls `setState({...})` with a fresh object on every run. New
19
+ * identity → effect re-runs → new state object → re-render → new identity: an
20
+ * unbreakable render loop, synchronous enough to hang inside `render()`.
21
+ *
22
+ * So the fallback documented as the graceful path for consumers mounted outside
23
+ * a provider was the one thing that made those consumers impossible to mount —
24
+ * `record:alert` and `record:quick_actions` both call `useMetadataItem`
25
+ * unconditionally, and both spin. Found by
26
+ * `apps/console/src/__tests__/record-block-record-reach.test.tsx`
27
+ * (objectui#3149), which could not mount them at all until this was frozen.
28
+ *
29
+ * Frozen so a consumer cannot mutate the shared fallback out from under every
30
+ * other consumer of it.
31
+ */
32
+ const NO_METADATA_PROVIDER = Object.freeze({
33
+ apps: NO_ITEMS,
34
+ objects: NO_ITEMS,
35
+ dashboards: NO_ITEMS,
36
+ reports: NO_ITEMS,
37
+ pages: NO_ITEMS,
38
+ loading: false,
39
+ error: null,
40
+ refresh: async () => { },
41
+ invalidate: () => { },
42
+ ensureType: async () => [],
43
+ getItem: async () => null,
44
+ getItemsByType: () => [],
45
+ getTypeStatus: () => 'ready',
46
+ });
10
47
  export function useMetadata() {
11
48
  const ctx = useContext(MetadataCtx);
12
- if (!ctx) {
13
- // Graceful fallback: when a consumer is rendered outside a MetadataProvider
14
- // (common in unit tests that only need to assert on rendering), return an
15
- // empty no-op implementation rather than crash. Production code paths
16
- // should always wrap in <MetadataProvider>.
17
- return {
18
- apps: [],
19
- objects: [],
20
- dashboards: [],
21
- reports: [],
22
- pages: [],
23
- loading: false,
24
- error: null,
25
- refresh: async () => { },
26
- invalidate: () => { },
27
- ensureType: async () => [],
28
- getItem: async () => null,
29
- getItemsByType: () => [],
30
- getTypeStatus: () => 'ready',
31
- };
32
- }
33
- return ctx;
49
+ // Graceful fallback: a consumer rendered outside a MetadataProvider (unit
50
+ // tests that only assert on rendering, standalone previews) gets an empty
51
+ // no-op implementation rather than a crash. Production code paths should
52
+ // always wrap in <MetadataProvider>.
53
+ return ctx ?? NO_METADATA_PROVIDER;
34
54
  }
35
55
  export function useMetadataItem(type, name) {
36
56
  const { getItem } = useMetadata();
@@ -41,7 +61,15 @@ export function useMetadataItem(type, name) {
41
61
  });
42
62
  useEffect(() => {
43
63
  if (!name) {
44
- setState({ item: null, loading: false, error: null });
64
+ // Bail out when already cleared instead of installing a fresh object.
65
+ // Belt-and-braces against the loop the frozen fallback above fixes at the
66
+ // source: `getItem` is an effect dep, so ANY caller whose context value
67
+ // is rebuilt per render — a hand-rolled one in a test, which this
68
+ // interface explicitly invites — would otherwise re-run this effect,
69
+ // re-set an equal-but-new state object, and re-render forever.
70
+ setState((s) => (s.item === null && !s.loading && s.error === null
71
+ ? s
72
+ : { item: null, loading: false, error: null }));
45
73
  return;
46
74
  }
47
75
  let cancelled = false;
@@ -22,6 +22,34 @@
22
22
  * context for the draft/editing flags and drives `saving`/`error`/`reset`.
23
23
  */
24
24
  import React from 'react';
25
+ /**
26
+ * Server-computed decision aggregation of the record's pending approval node
27
+ * (framework#3266, `decision_progress` on `GET /approvals/requests/:id`).
28
+ *
29
+ * A multi-approver node does NOT finalize on the first decision: `unanimous`
30
+ * needs everyone, `quorum` needs `minApprovals` of the slate, `per_group` (会签)
31
+ * needs each named group to sign. Without the tally, an approver looking at the
32
+ * record cannot tell whether their own click completes the step or is one of
33
+ * three — the fact the whole node is about (objectstack#4478). The server does
34
+ * the counting so every client renders the same "2 of 3" the engine enforces.
35
+ *
36
+ * Absent for `first_response` nodes (one decision finalizes, so there is no
37
+ * progress to show) and for a backend that predates the enrichment.
38
+ */
39
+ export interface ApprovalProgress {
40
+ behavior: 'unanimous' | 'quorum' | 'per_group';
41
+ /** Approvals recorded so far — satisfied GROUPS when `behavior` is `per_group`. */
42
+ got: number;
43
+ /** Approvals required to finalize — total GROUPS when `behavior` is `per_group`. */
44
+ need: number;
45
+ /** Per-group tally, `per_group` only. */
46
+ groups?: Array<{
47
+ group: string;
48
+ got: number;
49
+ need: number;
50
+ satisfied: boolean;
51
+ }>;
52
+ }
25
53
  export interface InlineEditContextValue {
26
54
  /** True while the record is in inline-edit mode. */
27
55
  editing: boolean;
@@ -58,6 +86,15 @@ export interface InlineEditContextValue {
58
86
  * still get a coherent band. Defaults to `false`.
59
87
  */
60
88
  approvalPending: boolean;
89
+ /**
90
+ * Quorum / per-group tally of the pending approval node, when it aggregates
91
+ * more than one decision (objectstack#4478). Threaded verbatim from the
92
+ * host's approvals read so the band renders the server's count instead of
93
+ * re-deriving the engine's tally rules. Undefined when no approval is
94
+ * running, when the node finalizes on the first response, or when the host
95
+ * doesn't resolve approvals at all.
96
+ */
97
+ approvalProgress?: ApprovalProgress;
61
98
  /**
62
99
  * Human-readable reason for the approval lock, surfaced as the band's
63
100
  * tooltip. Optional — consumers fall back to their own localized default
@@ -109,6 +146,12 @@ export interface InlineEditProviderProps {
109
146
  * only know about the lock rendering exactly as before.
110
147
  */
111
148
  approvalPending?: boolean;
149
+ /**
150
+ * The pending node's server-computed decision tally (objectstack#4478).
151
+ * Surfaced verbatim so the band can show "2 of 3" / per-group ticks. Omitted
152
+ * for `first_response` nodes and for hosts that don't read approvals.
153
+ */
154
+ approvalProgress?: ApprovalProgress;
112
155
  /** Optional human-readable lock reason, surfaced as the band tooltip. */
113
156
  lockedReason?: string;
114
157
  children: React.ReactNode;
@@ -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, locked = false, approvalPending, lockedReason, children, }) => {
27
+ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPending, approvalProgress, lockedReason, children, }) => {
28
28
  // A locked record always has an approval running; the converse is what
29
29
  // #2902 added. Defaulting this way means an un-migrated host keeps its
30
30
  // previous behavior with no code change.
@@ -62,6 +62,7 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPen
62
62
  canEdit,
63
63
  locked,
64
64
  approvalPending: pending,
65
+ approvalProgress,
65
66
  lockedReason,
66
67
  draft,
67
68
  autoFocusField,
@@ -73,7 +74,7 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPen
73
74
  reset: teardown,
74
75
  setSaving,
75
76
  setError,
76
- }), [editing, canEdit, locked, pending, lockedReason, draft, autoFocusField, saving, error, enter, setField, teardown]);
77
+ }), [editing, canEdit, locked, pending, approvalProgress, lockedReason, draft, autoFocusField, saving, error, enter, setField, teardown]);
77
78
  return _jsx(InlineEditContext.Provider, { value: value, children: children });
78
79
  };
79
80
  /**
@@ -5,21 +5,34 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
+ import type { NavigationConfigSchema } from '@objectstack/spec/ui';
9
+ import type { SpecAuthoredInput } from '../spec-input';
8
10
  /**
9
- * Inline ViewNavigationConfig to avoid importing from @object-ui/types
10
- * (which may not be a direct dependency of @object-ui/react).
11
- * Mirrors the canonical definition in @object-ui/types/objectql.ts.
11
+ * The spec's `NavigationConfigSchema`, authoring side, with `mode` required.
12
+ *
13
+ * This was a hand copy carrying the note "inline … to avoid importing from
14
+ * @object-ui/types (which may not be a direct dependency of @object-ui/react)".
15
+ * That reasoning is about the wrong package: the vocabulary belongs to
16
+ * `@objectstack/spec`, which IS a direct dependency of this one — the same
17
+ * expired "kept local to avoid a dependency" comment objectui#3169 found in
18
+ * `@object-ui/app-shell`, where the dependency had likewise been there all
19
+ * along. Check `package.json` before believing such a note (objectstack#4115).
20
+ *
21
+ * The ONE divergence: the spec defaults `mode`, so its authoring side makes it
22
+ * optional; this hook dispatches on `mode` and its callers always supply one,
23
+ * so it is required here. Every other key is the spec's, by reference. Pinned
24
+ * by `__tests__/offline-nav-performance-spec-parity.test.ts`.
25
+ *
26
+ * Two per-key notes the hand copy carried, kept here because a derived alias
27
+ * has no members to hang them on:
28
+ * - `size` is the coarse overlay bucket added by #2578; `resolveOverlayWidth`
29
+ * below maps it through {@link OVERLAY_SIZE_WIDTHS}.
30
+ * - `width` is DEPRECATED by #2578 in favour of `size`. It still wins when
31
+ * present, because app-shell pre-resolves `size` into it.
12
32
  */
13
- export interface NavigationConfig {
14
- mode: 'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none';
15
- view?: string;
16
- preventNavigation?: boolean;
17
- openNewTab?: boolean;
18
- /** Spec `NavigationConfig.size` (#2578) — coarse overlay bucket. */
19
- size?: 'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
20
- /** @deprecated [#2578 → `size`] explicit pixel/percent width. */
21
- width?: string | number;
22
- }
33
+ export type NavigationConfig = Omit<SpecAuthoredInput<typeof NavigationConfigSchema>, 'mode'> & {
34
+ mode: NonNullable<SpecAuthoredInput<typeof NavigationConfigSchema>['mode']>;
35
+ };
23
36
  /**
24
37
  * Resolve the overlay width from a NavigationConfig: an explicit `width` wins
25
38
  * (app-shell pre-resolves `size` into it); otherwise a declared bucket maps
@@ -5,14 +5,25 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
+ import type { OfflineStrategy, ConflictResolution, PersistStorage, EvictionPolicy, OfflineCacheConfigSchema, OfflineConfigSchema, SyncConfigSchema } from '@objectstack/spec/ui';
9
+ import type { SpecAuthoredInput } from '../spec-input';
8
10
  /** Offline strategy determines how data is fetched when connectivity is limited. */
9
- export type OfflineStrategy = 'cache_first' | 'network_first' | 'stale_while_revalidate' | 'network_only' | 'cache_only';
10
- /** Conflict resolution strategy for sync operations. */
11
- export type ConflictResolutionStrategy = 'manual' | 'client_wins' | 'server_wins' | 'last_write_wins';
11
+ export type { OfflineStrategy };
12
+ /**
13
+ * Conflict resolution strategy for sync operations.
14
+ *
15
+ * Renamed from `ConflictResolutionStrategy` — that name belongs to a DIFFERENT
16
+ * spec export (`@objectstack/spec/api`: `error | priority | first-wins |
17
+ * last-wins`, the metadata-merge policy). The spec's name for the union this
18
+ * hook actually uses is `ConflictResolution`, so the fix was to take the spec's
19
+ * own name and its binding at once, the same move `FieldGroup` →
20
+ * `ObjectFieldGroup` made in objectui#3169.
21
+ */
22
+ export type { ConflictResolution };
12
23
  /** Persist storage backend. */
13
- export type PersistStorageType = 'indexeddb' | 'localstorage' | 'sqlite';
24
+ export type PersistStorageType = PersistStorage;
14
25
  /** Eviction policy for cache management. */
15
- export type EvictionPolicyType = 'lru' | 'lfu' | 'fifo';
26
+ export type EvictionPolicyType = EvictionPolicy;
16
27
  /** Sync state of the offline system. */
17
28
  export type SyncState = 'idle' | 'syncing' | 'error' | 'offline';
18
29
  /** A queued mutation waiting to be synced. */
@@ -23,31 +34,38 @@ export interface QueuedMutation {
23
34
  resource: string;
24
35
  data?: Record<string, unknown>;
25
36
  }
26
- /** Cache configuration aligned with OfflineCacheConfigSchema. */
27
- export interface OfflineCacheConfig {
28
- maxSize?: number;
29
- ttl?: number;
30
- persistStorage?: PersistStorageType;
31
- evictionPolicy?: EvictionPolicyType;
32
- }
33
- /** Sync configuration aligned with SyncConfigSchema. */
34
- export interface OfflineSyncConfig {
35
- strategy?: OfflineStrategy;
36
- conflictResolution?: ConflictResolutionStrategy;
37
- retryInterval?: number;
38
- maxRetries?: number;
39
- batchSize?: number;
40
- }
41
- /** Top-level offline configuration aligned with OfflineConfigSchema. */
42
- export interface OfflineConfig {
43
- enabled?: boolean;
44
- strategy?: OfflineStrategy;
45
- cache?: OfflineCacheConfig;
46
- sync?: OfflineSyncConfig;
47
- offlineIndicator?: boolean;
48
- offlineMessage?: string;
49
- queueMaxSize?: number;
50
- }
37
+ /**
38
+ * Cache configuration — the AUTHORING side of the spec's
39
+ * `OfflineCacheConfigSchema`.
40
+ *
41
+ * `persistStorage` and `evictionPolicy` carry `.default()`s, so they are
42
+ * optional to write and present after a parse. This hook is handed what an app
43
+ * author wrote, hence the input side; see {@link SpecAuthoredInput}.
44
+ */
45
+ export type OfflineCacheConfig = SpecAuthoredInput<typeof OfflineCacheConfigSchema>;
46
+ /**
47
+ * Sync configuration — the AUTHORING side of the spec's `SyncConfigSchema`.
48
+ *
49
+ * `strategy` and `conflictResolution` carry `.default()`s, so the output type
50
+ * makes them required; this hook is handed what an app author wrote.
51
+ */
52
+ export type OfflineSyncConfig = SpecAuthoredInput<typeof SyncConfigSchema>;
53
+ /**
54
+ * Top-level offline configuration — the AUTHORING side of the spec's
55
+ * `OfflineConfigSchema`.
56
+ *
57
+ * This name stays with `@object-ui/react` (objectui#3156 / objectui#3159): the
58
+ * `OfflineConfig` that used to sit in `@object-ui/types` was a service-worker
59
+ * ROUTE cache and has been renamed `PWAOfflineConfig`, so there is no
60
+ * cross-package clash left — this hook's config is the spec's concept, key for
61
+ * key, and takes the spec's binding.
62
+ *
63
+ * Input side, not `z.infer`, and here the reason is visible in the signature:
64
+ * `useOffline(config: OfflineConfig = {})` defaults to the empty object, which
65
+ * the output type — where `enabled`, `strategy` and `offlineIndicator` are all
66
+ * required once their `.default()`s have run — would reject outright.
67
+ */
68
+ export type OfflineConfig = SpecAuthoredInput<typeof OfflineConfigSchema>;
51
69
  /** Result returned by the useOffline hook. */
52
70
  export interface OfflineResult {
53
71
  /** Whether the browser is currently online. */
@@ -72,9 +90,9 @@ export interface OfflineResult {
72
90
  offlineMessage: string;
73
91
  }
74
92
  /**
75
- * Hook for offline mode detection and sync queue management.
76
- * Implements OfflineConfigSchema, SyncConfigSchema, and ConflictResolutionSchema
77
- * from @objectstack/spec v2.0.7.
93
+ * Hook for offline mode detection and sync queue management. Its config types
94
+ * ARE the spec's `OfflineConfigSchema` / `SyncConfigSchema` / `ConflictResolution`
95
+ * (authoring side) — see {@link OfflineConfig}.
78
96
  *
79
97
  * @example
80
98
  * ```tsx
@@ -66,9 +66,9 @@ const DEFAULTS = {
66
66
  queueMaxSize: 100,
67
67
  };
68
68
  /**
69
- * Hook for offline mode detection and sync queue management.
70
- * Implements OfflineConfigSchema, SyncConfigSchema, and ConflictResolutionSchema
71
- * from @objectstack/spec v2.0.7.
69
+ * Hook for offline mode detection and sync queue management. Its config types
70
+ * ARE the spec's `OfflineConfigSchema` / `SyncConfigSchema` / `ConflictResolution`
71
+ * (authoring side) — see {@link OfflineConfig}.
72
72
  *
73
73
  * @example
74
74
  * ```tsx
@@ -13,7 +13,7 @@ export interface VirtualScrollConfig {
13
13
  itemHeight?: number;
14
14
  overscan?: number;
15
15
  }
16
- /** Performance configuration aligned with PerformanceConfigSchema. */
16
+ /** Performance configuration. */
17
17
  export interface PerformanceConfig {
18
18
  /** Whether to lazy-load components/data. */
19
19
  lazyLoad?: boolean;
@@ -55,8 +55,8 @@ export interface PerformanceResult {
55
55
  debounce: <T extends (...args: unknown[]) => void>(fn: T) => T;
56
56
  }
57
57
  /**
58
- * Hook for performance monitoring and configuration aligned with
59
- * PerformanceConfigSchema from @objectstack/spec v2.0.7.
58
+ * Hook for performance monitoring and configuration. Its config type IS the
59
+ * spec's `PerformanceConfigSchema` (authoring side) — see {@link PerformanceConfig}.
60
60
  *
61
61
  * Provides resolved config values, Web Vitals metrics, and utility functions
62
62
  * (debounce, render marking) for performance-aware components.
@@ -52,8 +52,8 @@ function getLCP() {
52
52
  // usePerformance hook
53
53
  // ---------------------------------------------------------------------------
54
54
  /**
55
- * Hook for performance monitoring and configuration aligned with
56
- * PerformanceConfigSchema from @objectstack/spec v2.0.7.
55
+ * Hook for performance monitoring and configuration. Its config type IS the
56
+ * spec's `PerformanceConfigSchema` (authoring side) — see {@link PerformanceConfig}.
57
57
  *
58
58
  * Provides resolved config values, Web Vitals metrics, and utility functions
59
59
  * (debounce, render marking) for performance-aware components.
@@ -0,0 +1,37 @@
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
+ * The AUTHORING ("input") type of a `@objectstack/spec` zod schema.
10
+ *
11
+ * ## Why not `z.infer`
12
+ *
13
+ * `z.infer` is the OUTPUT type — what `.parse()` returns, with every
14
+ * `.default()` already applied and therefore REQUIRED. The hooks in this
15
+ * package are handed configuration an app author wrote by hand, so the input
16
+ * side is the one that describes what they may legally omit. Deriving a config
17
+ * type from `z.infer` turns every defaulted key into a required one and makes
18
+ * the schema's own defaults unusable — the `_input`/`_output` trap the spec
19
+ * symbol guard's header warns about, and the one that already bit
20
+ * `ObjectFieldGroup` in objectui#3169.
21
+ *
22
+ * Rule of thumb: is this module WRITING the metadata, or READING what has
23
+ * already been parsed? Writing → input. Reading → `z.infer`.
24
+ *
25
+ * ## Why it reads `_zod`, not `z.input`
26
+ *
27
+ * `z.input<typeof S>` needs a `zod` import, and `@object-ui/react` deliberately
28
+ * does not depend on zod — it renders plain objects and never parses. The
29
+ * schema's own type already carries both sides in `_zod`, so the input type is
30
+ * reachable through the spec's binding alone, with no new dependency and no
31
+ * copy of the shape.
32
+ */
33
+ export type SpecAuthoredInput<S extends {
34
+ _zod: {
35
+ input: unknown;
36
+ };
37
+ }> = S['_zod']['input'];
@@ -0,0 +1,8 @@
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
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/react",
3
- "version": "17.1.0",
3
+ "version": "17.2.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": "^17.0.0-rc.0",
27
+ "@objectstack/spec": "^17.0.0-rc.1",
28
28
  "react-hook-form": "^7.83.0",
29
- "@object-ui/core": "17.1.0",
30
- "@object-ui/data-objectstack": "17.1.0",
31
- "@object-ui/i18n": "17.1.0",
32
- "@object-ui/types": "17.1.0"
29
+ "@object-ui/core": "17.2.0",
30
+ "@object-ui/data-objectstack": "17.2.0",
31
+ "@object-ui/i18n": "17.2.0",
32
+ "@object-ui/types": "17.2.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^18.0.0 || ^19.0.0",
@@ -65,7 +65,7 @@
65
65
  "scripts": {
66
66
  "build": "tsc",
67
67
  "test": "vitest run",
68
- "type-check": "tsc --noEmit",
68
+ "type-check": "tsc --noEmit && tsc -p tsconfig.typetests.json",
69
69
  "lint": "eslint ."
70
70
  }
71
71
  }