@object-ui/react 17.3.0 → 17.5.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 +908 -0
- package/README.md +39 -13
- package/dist/SchemaRenderer.d.ts +78 -5
- package/dist/SchemaRenderer.js +139 -13
- package/dist/context/AppShellContext.d.ts +21 -2
- package/dist/context/RecordContext.d.ts +0 -4
- package/dist/context/RecordContext.js +0 -2
- package/dist/context/RelatedRecordActionsContext.d.ts +29 -0
- package/dist/context/RelatedRecordActionsContext.js +6 -0
- package/dist/element-data-source/ElementDataSourceGate.d.ts +199 -0
- package/dist/element-data-source/ElementDataSourceGate.js +195 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +10 -0
- package/dist/hooks/useActionTextLocalizer.d.ts +69 -0
- package/dist/hooks/useActionTextLocalizer.js +91 -0
- package/dist/hooks/useDatasetDimensionLabels.d.ts +104 -0
- package/dist/hooks/useDatasetDimensionLabels.js +154 -0
- package/dist/hooks/useElementDataSource.d.ts +68 -0
- package/dist/hooks/useElementDataSource.js +138 -0
- package/dist/hooks/useExpression.d.ts +58 -0
- package/dist/hooks/useExpression.js +65 -0
- package/dist/hooks/useNavigationOverlay.d.ts +58 -10
- package/dist/hooks/useOffline.d.ts +62 -36
- package/dist/hooks/useOffline.js +2 -2
- package/dist/hooks/usePageAssignment.js +4 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/schema-input.d.ts +31 -0
- package/dist/schema-input.js +31 -0
- package/dist/spec-bridge/SpecBridge.d.ts +11 -5
- package/dist/spec-bridge/SpecBridge.js +7 -1
- package/dist/spec-bridge/bridges/list-view.js +106 -14
- package/dist/spec-bridge/types.d.ts +24 -3
- package/dist/utils/i18n.d.ts +43 -8
- package/dist/utils/i18n.js +42 -4
- package/package.json +8 -8
|
@@ -5,10 +5,11 @@
|
|
|
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';
|
|
8
|
+
import type { NavigationConfigSchema, NavigationMode as SpecNavigationMode } from '@objectstack/spec/ui';
|
|
9
9
|
import type { SpecAuthoredInput } from '../spec-input';
|
|
10
10
|
/**
|
|
11
|
-
* The spec's `NavigationConfigSchema`, authoring side, with
|
|
11
|
+
* The spec's `NavigationConfigSchema`, authoring side — by reference, with no
|
|
12
|
+
* divergence of its own.
|
|
12
13
|
*
|
|
13
14
|
* This was a hand copy carrying the note "inline … to avoid importing from
|
|
14
15
|
* @object-ui/types (which may not be a direct dependency of @object-ui/react)".
|
|
@@ -18,10 +19,35 @@ import type { SpecAuthoredInput } from '../spec-input';
|
|
|
18
19
|
* `@object-ui/app-shell`, where the dependency had likewise been there all
|
|
19
20
|
* along. Check `package.json` before believing such a note (objectstack#4115).
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* `mode` is OPTIONAL, because the spec says so and this hook agrees.
|
|
23
|
+
* `packages/spec/src/ui/view.zod.ts` declares
|
|
24
|
+
* `mode: NavigationModeSchema.default('page')`, and a `.default()` lands on the
|
|
25
|
+
* AUTHORING side as `| undefined` — so `navigation: { view: 'summary_view' }`
|
|
26
|
+
* is legal authored metadata that lets the mode default.
|
|
27
|
+
*
|
|
28
|
+
* Until objectui#4550 this alias `Omit`ted `mode` and re-added it as
|
|
29
|
+
* `NonNullable<…>`, on the stated reasoning that "this hook dispatches on
|
|
30
|
+
* `mode` and its callers always supply one". Both halves were false.
|
|
31
|
+
* `useNavigationOverlay` does not require `mode` — it DEFAULTS it
|
|
32
|
+
* (`navigation?.mode ?? 'page'`, ~140 lines below), and `'page'` is meaningful
|
|
33
|
+
* behaviour rather than a placeholder. And callers did not always supply one:
|
|
34
|
+
* `ListView` carried `schema.navigation as NavigationConfig | undefined` purely
|
|
35
|
+
* to get a spec-shaped value past this declaration — a value the hook had
|
|
36
|
+
* always been willing to take.
|
|
37
|
+
*
|
|
38
|
+
* The rule that makes this a producer-side fix rather than a caller-side one:
|
|
39
|
+
* a type in front of an implementation must not be stricter than the
|
|
40
|
+
* implementation. When it is, every caller pays in assertions, and an assertion
|
|
41
|
+
* is exactly the renderer-side workaround AGENTS.md #0.1 sends back to the
|
|
42
|
+
* producer. Here the producer was this line.
|
|
43
|
+
*
|
|
44
|
+
* `@object-ui/types` re-exports the spec's own `NavigationConfig` unchanged, so
|
|
45
|
+
* that published name and this one now agree — they did not before, which is
|
|
46
|
+
* how one monorepo shipped two `NavigationConfig`s that disagreed about whether
|
|
47
|
+
* `mode` could be omitted. Pinned by
|
|
48
|
+
* `__tests__/offline-nav-performance-spec-parity.test.ts` (the type) and
|
|
49
|
+
* `__tests__/useNavigationOverlay.modeDefault.test.tsx` (the default behaviour
|
|
50
|
+
* the relaxation rests on).
|
|
25
51
|
*
|
|
26
52
|
* Two per-key notes the hand copy carried, kept here because a derived alias
|
|
27
53
|
* has no members to hang them on:
|
|
@@ -30,9 +56,7 @@ import type { SpecAuthoredInput } from '../spec-input';
|
|
|
30
56
|
* - `width` is DEPRECATED by #2578 in favour of `size`. It still wins when
|
|
31
57
|
* present, because app-shell pre-resolves `size` into it.
|
|
32
58
|
*/
|
|
33
|
-
export type NavigationConfig =
|
|
34
|
-
mode: NonNullable<SpecAuthoredInput<typeof NavigationConfigSchema>['mode']>;
|
|
35
|
-
};
|
|
59
|
+
export type NavigationConfig = SpecAuthoredInput<typeof NavigationConfigSchema>;
|
|
36
60
|
/**
|
|
37
61
|
* Resolve the overlay width from a NavigationConfig: an explicit `width` wins
|
|
38
62
|
* (app-shell pre-resolves `size` into it); otherwise a declared bucket maps
|
|
@@ -41,7 +65,31 @@ export type NavigationConfig = Omit<SpecAuthoredInput<typeof NavigationConfigSch
|
|
|
41
65
|
* count, which only schema-aware hosts have. Exported for the parity test.
|
|
42
66
|
*/
|
|
43
67
|
export declare function resolveOverlayWidth(navigation: NavigationConfig | undefined): string | number | undefined;
|
|
44
|
-
|
|
68
|
+
/**
|
|
69
|
+
* The overlay modes — the spec's own union, DERIVED since objectui#4167.
|
|
70
|
+
*
|
|
71
|
+
* rc.6 publishes `NavigationMode` (`z.input<typeof NavigationModeSchema>`), and
|
|
72
|
+
* this alias resolved to exactly it already: `NavigationConfig['mode']` was then
|
|
73
|
+
* `NonNullable<…['mode']>`, and stripping the `undefined` that the schema's
|
|
74
|
+
* `.default()` puts on the authoring side leaves the seven-member enum itself.
|
|
75
|
+
* So the spec reference was one hop away rather than absent — the alias just
|
|
76
|
+
* reached it through a member access, which reads as a hand-written union to
|
|
77
|
+
* `check:spec-symbols` and, more to the point, to a person.
|
|
78
|
+
*
|
|
79
|
+
* Bound to the spec directly instead: the seven members now arrive from the
|
|
80
|
+
* schema that validates them. `__tests__/offline-nav-performance-spec-parity.test.ts`
|
|
81
|
+
* pins that this stays the same type as `NonNullable<NavigationConfig['mode']>`,
|
|
82
|
+
* so the two spellings cannot silently come apart if the spec ever stops
|
|
83
|
+
* defaulting `mode` or defaults it on a narrower union.
|
|
84
|
+
*
|
|
85
|
+
* The `NonNullable` in that pin is objectui#4550's mark and is load-bearing:
|
|
86
|
+
* `NavigationConfig` no longer strips the authoring-side `undefined`, so
|
|
87
|
+
* `NavigationConfig['mode']` is `NavigationMode | undefined` and a bare
|
|
88
|
+
* equality would now fail for a reason unrelated to the drift being guarded.
|
|
89
|
+
* What stays pinned is the MEMBERSHIP — the seven modes this hook switches on
|
|
90
|
+
* are exactly the seven the exported union publishes.
|
|
91
|
+
*/
|
|
92
|
+
export type NavigationMode = SpecNavigationMode;
|
|
45
93
|
export interface UseNavigationOverlayOptions {
|
|
46
94
|
/** The navigation configuration from the schema */
|
|
47
95
|
navigation?: NavigationConfig;
|
|
@@ -5,25 +5,21 @@
|
|
|
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';
|
|
10
8
|
/** Offline strategy determines how data is fetched when connectivity is limited. */
|
|
11
|
-
export type
|
|
9
|
+
export type OfflineStrategy = 'cache_first' | 'network_first' | 'stale_while_revalidate' | 'network_only' | 'cache_only';
|
|
12
10
|
/**
|
|
13
11
|
* Conflict resolution strategy for sync operations.
|
|
14
12
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* last-wins`, the metadata-merge policy)
|
|
18
|
-
*
|
|
19
|
-
* own name and its binding at once, the same move `FieldGroup` →
|
|
20
|
-
* `ObjectFieldGroup` made in objectui#3169.
|
|
13
|
+
* Deliberately NOT named `ConflictResolutionStrategy` — that name belongs to a
|
|
14
|
+
* different contract (`@objectstack/spec/api`: `error | priority | first-wins |
|
|
15
|
+
* last-wins`, the metadata-merge policy), and the confusion between the two is
|
|
16
|
+
* what objectui#3169 / objectstack#4115 cost.
|
|
21
17
|
*/
|
|
22
|
-
export type
|
|
18
|
+
export type ConflictResolution = 'manual' | 'client_wins' | 'server_wins' | 'last_write_wins';
|
|
23
19
|
/** Persist storage backend. */
|
|
24
|
-
export type PersistStorageType =
|
|
20
|
+
export type PersistStorageType = 'sqlite' | 'indexeddb' | 'localstorage';
|
|
25
21
|
/** Eviction policy for cache management. */
|
|
26
|
-
export type EvictionPolicyType =
|
|
22
|
+
export type EvictionPolicyType = 'lru' | 'lfu' | 'fifo';
|
|
27
23
|
/** Sync state of the offline system. */
|
|
28
24
|
export type SyncState = 'idle' | 'syncing' | 'error' | 'offline';
|
|
29
25
|
/** A queued mutation waiting to be synced. */
|
|
@@ -35,37 +31,67 @@ export interface QueuedMutation {
|
|
|
35
31
|
data?: Record<string, unknown>;
|
|
36
32
|
}
|
|
37
33
|
/**
|
|
38
|
-
* Cache configuration
|
|
39
|
-
* `OfflineCacheConfigSchema`.
|
|
34
|
+
* Cache configuration.
|
|
40
35
|
*
|
|
41
|
-
* `persistStorage` and `evictionPolicy` carry `.default()`s, so
|
|
42
|
-
* optional to
|
|
43
|
-
*
|
|
36
|
+
* `persistStorage` and `evictionPolicy` used to carry schema `.default()`s, so
|
|
37
|
+
* they stay optional to WRITE — this hook is handed what an app author wrote,
|
|
38
|
+
* which is why every key here is optional (the former `SpecAuthoredInput` /
|
|
39
|
+
* input-side reading of the retired schema).
|
|
44
40
|
*/
|
|
45
|
-
export
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
export interface OfflineCacheConfig {
|
|
42
|
+
/** Maximum cache size. */
|
|
43
|
+
maxSize?: number;
|
|
44
|
+
/** Time-to-live for cached entries. */
|
|
45
|
+
ttl?: number;
|
|
46
|
+
/** Storage backend. Defaults to `indexeddb` when a cache is used. */
|
|
47
|
+
persistStorage?: PersistStorageType;
|
|
48
|
+
/** Eviction policy once `maxSize` is reached. Defaults to `lru`. */
|
|
49
|
+
evictionPolicy?: EvictionPolicyType;
|
|
50
|
+
}
|
|
51
|
+
/** Sync configuration — controls how queued mutations reach the server. */
|
|
52
|
+
export interface OfflineSyncConfig {
|
|
53
|
+
/** Fetch strategy for sync requests. */
|
|
54
|
+
strategy?: OfflineStrategy;
|
|
55
|
+
/** How to resolve a server/client conflict. */
|
|
56
|
+
conflictResolution?: ConflictResolution;
|
|
57
|
+
/** Delay between retry attempts, in ms. */
|
|
58
|
+
retryInterval?: number;
|
|
59
|
+
/** Maximum retry attempts before giving up. */
|
|
60
|
+
maxRetries?: number;
|
|
61
|
+
/** Mutations flushed per sync batch. */
|
|
62
|
+
batchSize?: number;
|
|
63
|
+
}
|
|
53
64
|
/**
|
|
54
|
-
* Top-level offline configuration
|
|
55
|
-
* `OfflineConfigSchema`.
|
|
65
|
+
* Top-level offline configuration.
|
|
56
66
|
*
|
|
57
67
|
* This name stays with `@object-ui/react` (objectui#3156 / objectui#3159): the
|
|
58
68
|
* `OfflineConfig` that used to sit in `@object-ui/types` was a service-worker
|
|
59
69
|
* ROUTE cache and has been renamed `PWAOfflineConfig`, so there is no
|
|
60
|
-
* cross-package clash
|
|
61
|
-
* key, and takes the spec's binding.
|
|
70
|
+
* cross-package clash.
|
|
62
71
|
*
|
|
63
|
-
*
|
|
64
|
-
* `useOffline(config: OfflineConfig = {})` defaults to the empty object,
|
|
65
|
-
*
|
|
66
|
-
*
|
|
72
|
+
* Every key is optional, and that is load-bearing rather than incidental:
|
|
73
|
+
* `useOffline(config: OfflineConfig = {})` defaults to the empty object, so a
|
|
74
|
+
* shape that made `enabled` / `strategy` / `offlineIndicator` required — as the
|
|
75
|
+
* retired schema's OUTPUT side did, once its `.default()`s had run — would
|
|
76
|
+
* reject the hook's own default argument. {@link DEFAULTS} below applies the
|
|
77
|
+
* same values the schema defaults used to.
|
|
67
78
|
*/
|
|
68
|
-
export
|
|
79
|
+
export interface OfflineConfig {
|
|
80
|
+
/** Whether offline mode is active. Defaults to `true`. */
|
|
81
|
+
enabled?: boolean;
|
|
82
|
+
/** Fetch strategy. Defaults to `network_first`. */
|
|
83
|
+
strategy?: OfflineStrategy;
|
|
84
|
+
/** Client-side cache configuration. */
|
|
85
|
+
cache?: OfflineCacheConfig;
|
|
86
|
+
/** Sync behaviour for queued mutations. */
|
|
87
|
+
sync?: OfflineSyncConfig;
|
|
88
|
+
/** Whether to surface the offline indicator UI. Defaults to `true`. */
|
|
89
|
+
offlineIndicator?: boolean;
|
|
90
|
+
/** Message shown while offline. */
|
|
91
|
+
offlineMessage?: string;
|
|
92
|
+
/** Maximum queued mutations retained. Defaults to `100`. */
|
|
93
|
+
queueMaxSize?: number;
|
|
94
|
+
}
|
|
69
95
|
/** Result returned by the useOffline hook. */
|
|
70
96
|
export interface OfflineResult {
|
|
71
97
|
/** Whether the browser is currently online. */
|
|
@@ -91,8 +117,8 @@ export interface OfflineResult {
|
|
|
91
117
|
}
|
|
92
118
|
/**
|
|
93
119
|
* Hook for offline mode detection and sync queue management. Its config types
|
|
94
|
-
*
|
|
95
|
-
*
|
|
120
|
+
* are declared locally — see {@link OfflineConfig} for why they stopped being
|
|
121
|
+
* the spec's.
|
|
96
122
|
*
|
|
97
123
|
* @example
|
|
98
124
|
* ```tsx
|
package/dist/hooks/useOffline.js
CHANGED
|
@@ -67,8 +67,8 @@ const DEFAULTS = {
|
|
|
67
67
|
};
|
|
68
68
|
/**
|
|
69
69
|
* Hook for offline mode detection and sync queue management. Its config types
|
|
70
|
-
*
|
|
71
|
-
*
|
|
70
|
+
* are declared locally — see {@link OfflineConfig} for why they stopped being
|
|
71
|
+
* the spec's.
|
|
72
72
|
*
|
|
73
73
|
* @example
|
|
74
74
|
* ```tsx
|
|
@@ -76,10 +76,10 @@ export function usePageAssignment(objectName, opts = {}) {
|
|
|
76
76
|
if (!p)
|
|
77
77
|
return false;
|
|
78
78
|
// Only `pageType: 'record'` (or bare `type: 'record'`) is a
|
|
79
|
-
// user-facing record-detail page. `pageType
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// otherwise opening any record would render
|
|
79
|
+
// user-facing record-detail page. Any other `pageType` — notably the
|
|
80
|
+
// designer-side `'record_detail'`, dropped from `PageType` in
|
|
81
|
+
// framework#2265 but still expressible in raw metadata — must NOT be
|
|
82
|
+
// picked up here, otherwise opening any record would render it.
|
|
83
83
|
const pt = p.pageType ?? (p.type === 'record' ? 'record' : undefined);
|
|
84
84
|
const isRecord = pt === 'record';
|
|
85
85
|
if (!isRecord)
|
package/dist/index.d.ts
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
export * from './SchemaRenderer';
|
|
9
|
+
export * from './schema-input';
|
|
9
10
|
export * from './hooks';
|
|
10
11
|
export * from './context';
|
|
11
12
|
export * from './LazyPluginLoader';
|
|
12
13
|
export * from './spec-bridge';
|
|
13
14
|
export * from './data-invalidation';
|
|
14
|
-
export
|
|
15
|
+
export * from './element-data-source/ElementDataSourceGate';
|
|
16
|
+
export { resolveKeyedI18nLabel } from './utils/i18n';
|
|
15
17
|
export { extractWriteErrorMessage, isPermissionError, extractFieldErrors } from './utils/error-message';
|
|
16
18
|
export type { WriteFieldError } from './utils/error-message';
|
|
17
19
|
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
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
export * from './SchemaRenderer';
|
|
9
|
+
export * from './schema-input';
|
|
9
10
|
export * from './hooks'; // will be empty for now
|
|
10
11
|
export * from './context'; // will be empty for now
|
|
11
12
|
export * from './LazyPluginLoader';
|
|
12
13
|
export * from './spec-bridge';
|
|
13
14
|
export * from './data-invalidation';
|
|
15
|
+
// PageComponentSchema.dataSource — mapping the spec's per-element data binding
|
|
16
|
+
// onto the schema keys each object-bound block reads (objectstack#6953).
|
|
17
|
+
export * from './element-data-source/ElementDataSourceGate';
|
|
14
18
|
// i18n utilities
|
|
15
|
-
export {
|
|
19
|
+
export { resolveKeyedI18nLabel } from './utils/i18n';
|
|
16
20
|
// Write-error surfacing utilities (shared by drag-write plugins so a failed
|
|
17
21
|
// PATCH — e.g. an RLS 403 — is never silently swallowed).
|
|
18
22
|
export { extractWriteErrorMessage, isPermissionError, extractFieldErrors } from './utils/error-message';
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
import type { BaseSchema } from '@object-ui/types';
|
|
9
|
+
import type { SchemaRendererProps } from './SchemaRenderer';
|
|
10
|
+
/**
|
|
11
|
+
* Narrow a loosely-typed metadata node onto {@link SchemaRendererProps.schema}.
|
|
12
|
+
*
|
|
13
|
+
* `@object-ui/types` declares `SchemaNode` as
|
|
14
|
+
* `BaseSchema | string | number | boolean | null | undefined`, and a lot of
|
|
15
|
+
* metadata plumbing (page regions, header-bar actions, detail tabs, view
|
|
16
|
+
* configs) is typed with it. `SchemaRenderer` deliberately does NOT declare the
|
|
17
|
+
* `number` / `boolean` members — nobody should be invited to author them — so
|
|
18
|
+
* forwarding such a value needs one honest step in between.
|
|
19
|
+
*
|
|
20
|
+
* This is that step, and it is a TOTAL function rather than a cast: it maps the
|
|
21
|
+
* two primitive members onto their text form, which is precisely what the
|
|
22
|
+
* renderer's own defensive branch does with them. So it changes no behaviour and
|
|
23
|
+
* tells no lie — the value a caller forwards renders identically whether or not
|
|
24
|
+
* it passes through here.
|
|
25
|
+
*
|
|
26
|
+
* It exists because the two competing repo-wide `SchemaNode` spellings
|
|
27
|
+
* (`@object-ui/core`'s interface vs `@object-ui/types`' union) have not been
|
|
28
|
+
* reconciled; that reconciliation is tracked separately. When it lands, the
|
|
29
|
+
* call sites using this can go back to forwarding directly.
|
|
30
|
+
*/
|
|
31
|
+
export declare function toRenderableSchema(node: BaseSchema | string | number | boolean | null | undefined): SchemaRendererProps['schema'];
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
* Narrow a loosely-typed metadata node onto {@link SchemaRendererProps.schema}.
|
|
10
|
+
*
|
|
11
|
+
* `@object-ui/types` declares `SchemaNode` as
|
|
12
|
+
* `BaseSchema | string | number | boolean | null | undefined`, and a lot of
|
|
13
|
+
* metadata plumbing (page regions, header-bar actions, detail tabs, view
|
|
14
|
+
* configs) is typed with it. `SchemaRenderer` deliberately does NOT declare the
|
|
15
|
+
* `number` / `boolean` members — nobody should be invited to author them — so
|
|
16
|
+
* forwarding such a value needs one honest step in between.
|
|
17
|
+
*
|
|
18
|
+
* This is that step, and it is a TOTAL function rather than a cast: it maps the
|
|
19
|
+
* two primitive members onto their text form, which is precisely what the
|
|
20
|
+
* renderer's own defensive branch does with them. So it changes no behaviour and
|
|
21
|
+
* tells no lie — the value a caller forwards renders identically whether or not
|
|
22
|
+
* it passes through here.
|
|
23
|
+
*
|
|
24
|
+
* It exists because the two competing repo-wide `SchemaNode` spellings
|
|
25
|
+
* (`@object-ui/core`'s interface vs `@object-ui/types`' union) have not been
|
|
26
|
+
* reconciled; that reconciliation is tracked separately. When it lands, the
|
|
27
|
+
* call sites using this can go back to forwarding directly.
|
|
28
|
+
*/
|
|
29
|
+
export function toRenderableSchema(node) {
|
|
30
|
+
return typeof node === 'number' || typeof node === 'boolean' ? String(node) : node;
|
|
31
|
+
}
|
|
@@ -5,18 +5,24 @@
|
|
|
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 {
|
|
8
|
+
import type { BaseSchema } from '@object-ui/types';
|
|
9
9
|
import type { BridgeContext, BridgeFn } from './types';
|
|
10
10
|
export declare class SpecBridge {
|
|
11
11
|
private bridges;
|
|
12
12
|
private context;
|
|
13
13
|
constructor(context?: BridgeContext);
|
|
14
14
|
register(specType: string, bridge: BridgeFn): void;
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Transform a spec schema into a schema node tree.
|
|
17
|
+
*
|
|
18
|
+
* Returns `BaseSchema` rather than `SchemaNode` — see {@link BridgeFn} for
|
|
19
|
+
* the ruling (objectui#4580 Q4-B) and the measurement behind it. A registered
|
|
20
|
+
* bridge always produces an object; the union only made callers narrow.
|
|
21
|
+
*/
|
|
22
|
+
transform(specType: string, spec: any): BaseSchema;
|
|
17
23
|
/** Transform a ListView spec */
|
|
18
|
-
transformListView(spec: any):
|
|
24
|
+
transformListView(spec: any): BaseSchema;
|
|
19
25
|
/** Transform a FormView spec */
|
|
20
|
-
transformFormView(spec: any):
|
|
26
|
+
transformFormView(spec: any): BaseSchema;
|
|
21
27
|
updateContext(ctx: Partial<BridgeContext>): void;
|
|
22
28
|
}
|
|
@@ -28,7 +28,13 @@ export class SpecBridge {
|
|
|
28
28
|
register(specType, bridge) {
|
|
29
29
|
this.bridges.set(specType, bridge);
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Transform a spec schema into a schema node tree.
|
|
33
|
+
*
|
|
34
|
+
* Returns `BaseSchema` rather than `SchemaNode` — see {@link BridgeFn} for
|
|
35
|
+
* the ruling (objectui#4580 Q4-B) and the measurement behind it. A registered
|
|
36
|
+
* bridge always produces an object; the union only made callers narrow.
|
|
37
|
+
*/
|
|
32
38
|
transform(specType, spec) {
|
|
33
39
|
const bridge = this.bridges.get(specType);
|
|
34
40
|
if (!bridge) {
|
|
@@ -38,21 +38,113 @@ function mapColumn(col) {
|
|
|
38
38
|
mapped.action = col.action;
|
|
39
39
|
return mapped;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The spec's five row heights collapsed onto the renderer's three densities.
|
|
43
|
+
*
|
|
44
|
+
* `Record<RowHeight, …>` on purpose (objectui#4352): the key set is the spec's,
|
|
45
|
+
* so a row height added upstream fails the build here instead of silently
|
|
46
|
+
* arriving with no density. The table used to carry four more keys —
|
|
47
|
+
* `comfortable`, `spacious`, `small`, `large` — which `RowHeightSchema` does
|
|
48
|
+
* not admit, so no spec-valid list view could ever reach them; they only
|
|
49
|
+
* survived because the parameter was widened back to `string` and the fixture
|
|
50
|
+
* asserting three of them compiled against nothing. Deleting them is AGENTS.md
|
|
51
|
+
* #0.1: a renderer-side dialect for off-spec metadata is a second de-facto
|
|
52
|
+
* contract, and one strict contract beats N. An off-spec `rowHeight` now falls
|
|
53
|
+
* through to `undefined` — the producer is where it gets fixed.
|
|
54
|
+
*/
|
|
55
|
+
const ROW_HEIGHT_TO_DENSITY = {
|
|
56
|
+
compact: 'compact',
|
|
57
|
+
short: 'compact',
|
|
58
|
+
medium: 'comfortable',
|
|
59
|
+
tall: 'spacious',
|
|
60
|
+
extra_tall: 'spacious',
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Runtime reader for {@link ROW_HEIGHT_TO_DENSITY} — the five spec row heights
|
|
64
|
+
* and **nothing else**.
|
|
65
|
+
*
|
|
66
|
+
* `hasOwnProperty`, not a bare index (objectui#4442): the table is a plain
|
|
67
|
+
* object literal, so indexing it with an unchecked key reaches
|
|
68
|
+
* `Object.prototype`. The parameter is typed `RowHeight`, but the boundary a
|
|
69
|
+
* host's stored view definition actually crosses is `SpecBridge.transformListView`,
|
|
70
|
+
* whose parameter is `any` — so `rowHeight: 'toString'` used to come back as
|
|
71
|
+
* `Object.prototype.toString`, a FUNCTION returned from a signature that
|
|
72
|
+
* promises three strings or nothing. `bridgeListView` then writes the key under
|
|
73
|
+
* `if (density)`, and a function is truthy, so it landed on the SchemaNode.
|
|
74
|
+
*
|
|
75
|
+
* This is the same guard `@object-ui/core`'s `rowHeightToDensityMode` grew in
|
|
76
|
+
* objectui#4440, and the repo's existing convention (`freeze-schema.ts:135`,
|
|
77
|
+
* `metadata-admin/predicate.ts:305`, six more). Both rowHeight surfaces now
|
|
78
|
+
* abstain identically on every off-spec spelling; the agreement is pinned by
|
|
79
|
+
* `__tests__/RowHeightDensityAgreement.test.ts`.
|
|
80
|
+
*
|
|
81
|
+
* `typeof rowHeight !== 'string'`, not `!rowHeight` (objectui#4459): the
|
|
82
|
+
* truthiness guard this replaces rejected only FALSY values, so every truthy
|
|
83
|
+
* non-string walked past it into a lookup that coerces its key — both
|
|
84
|
+
* `hasOwnProperty.call` and the index run `String(...)`. `['compact']`, a boxed
|
|
85
|
+
* `String('compact')` and `{ toString: () => 'compact' }` therefore each
|
|
86
|
+
* selected a real density, where core's twin abstained
|
|
87
|
+
* (`packages/core/src/utils/normalize-list-view.ts:83`, which has opened with
|
|
88
|
+
* the type guard since #4440). Note the direction against #4442: that leak produced a
|
|
89
|
+
* FUNCTION, visibly wrong to everything downstream; this one produced a
|
|
90
|
+
* legitimate-looking `'compact'` that nothing can tell apart from an authored
|
|
91
|
+
* value. The type guard subsumes the old one — `undefined`, `null`, `0` and
|
|
92
|
+
* `false` are all non-strings — and `''` keeps its answer while changing route:
|
|
93
|
+
* a string, so it passes here and is refused by `hasOwnProperty` one line down.
|
|
94
|
+
*/
|
|
41
95
|
function mapDensity(rowHeight) {
|
|
42
|
-
if (
|
|
96
|
+
if (typeof rowHeight !== 'string')
|
|
43
97
|
return undefined;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
98
|
+
if (!Object.prototype.hasOwnProperty.call(ROW_HEIGHT_TO_DENSITY, rowHeight)) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return ROW_HEIGHT_TO_DENSITY[rowHeight];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The spec's own parse-time lift, applied where `parse` cannot reach
|
|
105
|
+
* (objectui#4585).
|
|
106
|
+
*
|
|
107
|
+
* `@objectstack/spec` accepts BOTH spellings of `view.exportOptions` and lifts
|
|
108
|
+
* the legacy bare format array to the object form at parse (objectstack#8010,
|
|
109
|
+
* `ui/view.zod.ts`):
|
|
110
|
+
*
|
|
111
|
+
* z.array(ListViewExportFormatSchema).transform((formats) => ({ formats }))
|
|
112
|
+
*
|
|
113
|
+
* That transform never runs on this path. The bridge's input is a TypeScript
|
|
114
|
+
* type, not a parsed value — there is no `parse`/`safeParse` anywhere under
|
|
115
|
+
* `spec-bridge/` — so a host that parses first hands over the object form while
|
|
116
|
+
* a host that forwards raw stored metadata hands over whatever was authored,
|
|
117
|
+
* and nothing here can tell them apart. The legacy array therefore used to
|
|
118
|
+
* reach the `object-grid` node verbatim, where `ObjectGrid` reads
|
|
119
|
+
* `exportOptions.formats` and only that: `.formats` on an array is `undefined`,
|
|
120
|
+
* the renderer's `['csv', 'json']` default won, and the view's declared formats
|
|
121
|
+
* were dropped with no error and no console line — the export button still
|
|
122
|
+
* showed, because a non-empty array is truthy.
|
|
123
|
+
*
|
|
124
|
+
* This mirrors that transform and NOTHING more: the same contract applied one
|
|
125
|
+
* layer out, not a second de-facto one (AGENTS.md #0.1 — the consumer-side
|
|
126
|
+
* `Array.isArray` fallback in the renderer is what that rule forbids). Hence:
|
|
127
|
+
*
|
|
128
|
+
* - the object form passes through by reference, unread and unrewritten;
|
|
129
|
+
* - an empty array lifts to `{ formats: [] }`, exactly as the spec's transform
|
|
130
|
+
* does — its `z.array()` carries no `.min(1)`, so `[]` is a legal input that
|
|
131
|
+
* wraps rather than defaults. ObjectGrid then offers no format and hides the
|
|
132
|
+
* export button: "declared zero formats", read literally;
|
|
133
|
+
* - a `'pdf'` stored before its retirement is carried, not filtered. The spec
|
|
134
|
+
* REFUSES `'pdf'` at parse with a migration prescription (objectstack#8010;
|
|
135
|
+
* PDF export itself was declined as objectstack#1301 NOT_PLANNED) — it does
|
|
136
|
+
* not silently drop the value, so neither may this. Such a format dies
|
|
137
|
+
* downstream in ObjectGrid's format-AGNOSTIC menu filter, kept deliberately
|
|
138
|
+
* at objectui#4535 for metadata predating `os migrate meta --from 16`.
|
|
139
|
+
*/
|
|
140
|
+
function liftExportOptions(exportOptions) {
|
|
141
|
+
if (!Array.isArray(exportOptions))
|
|
142
|
+
return exportOptions;
|
|
143
|
+
// The pinned `@objectstack/spec@17.0.0-rc.6` still admits `'pdf'` as an array
|
|
144
|
+
// member (the enum narrowing arrives with the pin bump), so the element type
|
|
145
|
+
// here is wider than the renderer-side `ListViewExportFormat`. Narrowing it by
|
|
146
|
+
// filtering would invent precisely the silent drop the spec declines to do.
|
|
147
|
+
return { formats: exportOptions };
|
|
56
148
|
}
|
|
57
149
|
/** Transforms a ListView spec into a DataTable SchemaNode */
|
|
58
150
|
export const bridgeListView = (spec, _context) => {
|
|
@@ -116,7 +208,7 @@ export const bridgeListView = (spec, _context) => {
|
|
|
116
208
|
if (spec.inlineEdit != null)
|
|
117
209
|
node.inlineEdit = spec.inlineEdit;
|
|
118
210
|
if (spec.exportOptions)
|
|
119
|
-
node.exportOptions = spec.exportOptions;
|
|
211
|
+
node.exportOptions = liftExportOptions(spec.exportOptions);
|
|
120
212
|
if (spec.emptyState)
|
|
121
213
|
node.emptyState = spec.emptyState;
|
|
122
214
|
if (spec.userActions)
|
|
@@ -5,7 +5,7 @@
|
|
|
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 {
|
|
8
|
+
import type { BaseSchema } from '@object-ui/types';
|
|
9
9
|
/** Context passed to all bridge functions */
|
|
10
10
|
export interface BridgeContext {
|
|
11
11
|
/** Current user context for permission checks */
|
|
@@ -30,5 +30,26 @@ export interface ObjectDefLite {
|
|
|
30
30
|
}>;
|
|
31
31
|
}>;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* A bridge function transforms a spec schema into a schema node tree.
|
|
35
|
+
*
|
|
36
|
+
* Returns **`BaseSchema`**, not `SchemaNode` (objectui#4580, ruling Q4-B).
|
|
37
|
+
* Once core's `SchemaNode` became `@object-ui/types`' union
|
|
38
|
+
* (`BaseSchema | string | number | boolean | null | undefined`), declaring the
|
|
39
|
+
* return as `SchemaNode` made every caller re-ask a question no bridge has ever
|
|
40
|
+
* answered "yes" to: *is this a bare string / null?* Both shipped bridges end
|
|
41
|
+
* in a single `return node` on an object literal — `bridges/list-view.ts:226`
|
|
42
|
+
* and `bridges/form-view.ts:195` — so the wider declaration described nothing
|
|
43
|
+
* real while forcing a narrowing at every read.
|
|
44
|
+
*
|
|
45
|
+
* That cost was measured, not assumed: round 1 of this card recorded **272**
|
|
46
|
+
* mechanical errors across five spec-bridge suites, all of them TS18049
|
|
47
|
+
* (`'node' is possibly 'null' or 'undefined'`) and TS2339
|
|
48
|
+
* (`Property 'formType' does not exist on type
|
|
49
|
+
* 'string | number | boolean | BaseSchema'`) — the union being destructured by
|
|
50
|
+
* tests reading properties off a node the bridge always produces.
|
|
51
|
+
*
|
|
52
|
+
* This is a type ANNOTATION only: no runtime behaviour changes, and the emitted
|
|
53
|
+
* JavaScript is byte-identical (proven by bundle sha256 on this PR).
|
|
54
|
+
*/
|
|
55
|
+
export type BridgeFn<T = any> = (spec: T, context: BridgeContext) => BaseSchema;
|
package/dist/utils/i18n.d.ts
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
|
+
import type { KeyedI18nLabel } from '@object-ui/types';
|
|
1
2
|
/**
|
|
2
|
-
* Resolves
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Resolves objectui's KEYED i18n label to a plain string.
|
|
4
|
+
*
|
|
5
|
+
* The keyed form is `{ key, defaultValue?, params? }` — a reference INTO a
|
|
6
|
+
* translation bundle. This package has no `t()`, so it resolves the reference
|
|
7
|
+
* as far as it can without one: `defaultValue`, else the key itself. (The
|
|
8
|
+
* app-shell twin of this function takes a `t` and resolves the key properly.)
|
|
9
|
+
*
|
|
10
|
+
* ## Why this is not called `resolveI18nLabel` (objectui#4167)
|
|
11
|
+
*
|
|
12
|
+
* `@objectstack/spec` 17.0.0-rc.6 publishes a function of that exact name from
|
|
13
|
+
* `@objectstack/spec/ui`, over a DIFFERENT vocabulary: the INLINE LOCALE MAP
|
|
14
|
+
* (`{ en: 'Owner', 'zh-CN': '负责人' }`), resolved against a BCP-47 locale
|
|
15
|
+
* (`resolveI18nLabel(label, locale)`). Two resolvers, one name, and neither
|
|
16
|
+
* accepts the other's shape — which objectstack#4115 calls a planted premise
|
|
17
|
+
* rather than a naming nit, because the next agent reads the name and builds
|
|
18
|
+
* on it.
|
|
19
|
+
*
|
|
20
|
+
* rc.6 is also what made the clash dangerous rather than merely untidy. It
|
|
21
|
+
* widened the spec's `I18nLabel` from `string` to
|
|
22
|
+
* `string | Record< string, string >`, so the SAME authored value can now reach
|
|
23
|
+
* either function — and each answers wrongly for the other's input, silently:
|
|
24
|
+
*
|
|
25
|
+
* this one, given `{ en: 'Owner' }` → `undefined` (no `key`, no
|
|
26
|
+
* `defaultValue`) → renders empty;
|
|
27
|
+
* the spec's, given `{ key: 'nav.home' }` → treats `key`/`defaultValue`/
|
|
28
|
+
* `params` as locale tags and picks
|
|
29
|
+
* one of them as the display text.
|
|
30
|
+
*
|
|
31
|
+
* Neither is a type error at the call site that matters, and both look like a
|
|
32
|
+
* working render until someone reads the output. PR #4169 met this head-on and
|
|
33
|
+
* aliased the spec's import as `resolveInlineI18nLabel` in five files, with
|
|
34
|
+
* hand-written comments explaining the clash at two of them — a review
|
|
35
|
+
* convention, which is exactly what objectstack#4115 exists to replace with a
|
|
36
|
+
* rule. `Keyed` is the counterpart of that `Inline`: the name now says which
|
|
37
|
+
* vocabulary it resolves, at every call site, with no comment required.
|
|
38
|
+
*
|
|
39
|
+
* The keyed shape itself is now named too — `KeyedI18nLabel` in
|
|
40
|
+
* `@object-ui/types` (#4581) — so `BaseSchema.ariaLabel`, this parameter and
|
|
41
|
+
* the layout twin all state one type instead of three copies of one object
|
|
42
|
+
* literal. The `Inline`/`Keyed` split above is the naming half of #4167; the
|
|
43
|
+
* named shape is the declaration half.
|
|
5
44
|
*/
|
|
6
|
-
export declare function
|
|
7
|
-
key: string;
|
|
8
|
-
defaultValue?: string;
|
|
9
|
-
params?: Record<string, any>;
|
|
10
|
-
} | undefined): string | undefined;
|
|
45
|
+
export declare function resolveKeyedI18nLabel(label: string | KeyedI18nLabel | undefined): string | undefined;
|