@object-ui/react 4.0.6 → 4.0.8
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 +44 -0
- package/dist/SchemaRenderer.d.ts +5 -0
- package/dist/SchemaRenderer.js +12 -1
- package/dist/hooks/useDensityMode.d.ts +18 -8
- package/dist/hooks/useDensityMode.js +41 -13
- package/dist/hooks/useExpression.d.ts +17 -0
- package/dist/hooks/useExpression.js +29 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @object-ui/react
|
|
2
2
|
|
|
3
|
+
## 4.0.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [3d58eaa]
|
|
8
|
+
- @object-ui/i18n@4.0.8
|
|
9
|
+
- @object-ui/types@4.0.8
|
|
10
|
+
- @object-ui/core@4.0.8
|
|
11
|
+
- @object-ui/data-objectstack@4.0.8
|
|
12
|
+
|
|
13
|
+
## 4.0.7
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 7c9b85c: Fix compatibility with the framework's normalized Expression envelope format.
|
|
18
|
+
|
|
19
|
+
`@objectstack/spec` now emits predicate (`visible` / `enabled`) and template
|
|
20
|
+
(`titleFormat`) fields as `{ dialect, source }` envelopes instead of bare
|
|
21
|
+
strings. The previous implementation assumed strings and crashed the record
|
|
22
|
+
detail view (`TypeError: titleFormat.replace is not a function`) and printed
|
|
23
|
+
`Failed to evaluate expression: ${[object Object]}` for every action visibility
|
|
24
|
+
predicate.
|
|
25
|
+
- `@object-ui/core`: `ExpressionEvaluator.evaluate` / `evaluateCondition` now
|
|
26
|
+
unwrap Expression envelopes transparently.
|
|
27
|
+
- `@object-ui/react`: new `toPredicateInput()` helper to safely normalize
|
|
28
|
+
`boolean | string | Expression` predicate inputs into the `${expr}` form
|
|
29
|
+
expected by `useCondition`.
|
|
30
|
+
- `@object-ui/components`: `action-bar`, `action-button`, `action-group`,
|
|
31
|
+
`action-icon`, `action-menu` renderers use `toPredicateInput()` instead of
|
|
32
|
+
template-literal interpolation that produced `${[object Object]}`.
|
|
33
|
+
- `@object-ui/plugin-detail`, `@object-ui/plugin-kanban`,
|
|
34
|
+
`@object-ui/plugin-calendar`, `@object-ui/app-shell`,
|
|
35
|
+
`@object-ui/console`: title-format helpers accept both legacy strings and
|
|
36
|
+
the new `{ source }` envelope.
|
|
37
|
+
|
|
38
|
+
All changes are backward-compatible — legacy bare strings continue to work.
|
|
39
|
+
|
|
40
|
+
- Updated dependencies [7c9b85c]
|
|
41
|
+
- Updated dependencies [fd15918]
|
|
42
|
+
- @object-ui/core@4.0.7
|
|
43
|
+
- @object-ui/i18n@4.0.7
|
|
44
|
+
- @object-ui/data-objectstack@4.0.7
|
|
45
|
+
- @object-ui/types@4.0.7
|
|
46
|
+
|
|
3
47
|
## 4.0.6
|
|
4
48
|
|
|
5
49
|
### Patch Changes
|
package/dist/SchemaRenderer.d.ts
CHANGED
|
@@ -19,9 +19,14 @@ interface SchemaErrorBoundaryState {
|
|
|
19
19
|
export declare class SchemaErrorBoundary extends Component<{
|
|
20
20
|
componentType?: string;
|
|
21
21
|
children: React.ReactNode;
|
|
22
|
+
resetKey?: any;
|
|
22
23
|
}, SchemaErrorBoundaryState> {
|
|
23
24
|
state: SchemaErrorBoundaryState;
|
|
24
25
|
static getDerivedStateFromError(error: Error): SchemaErrorBoundaryState;
|
|
26
|
+
componentDidUpdate(prevProps: {
|
|
27
|
+
componentType?: string;
|
|
28
|
+
resetKey?: any;
|
|
29
|
+
}): void;
|
|
25
30
|
handleRetry: () => void;
|
|
26
31
|
render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null | undefined;
|
|
27
32
|
}
|
package/dist/SchemaRenderer.js
CHANGED
|
@@ -53,6 +53,17 @@ export class SchemaErrorBoundary extends Component {
|
|
|
53
53
|
static getDerivedStateFromError(error) {
|
|
54
54
|
return { hasError: true, error };
|
|
55
55
|
}
|
|
56
|
+
componentDidUpdate(prevProps) {
|
|
57
|
+
// Auto-recover when the upstream component identity or an explicit reset
|
|
58
|
+
// key changes. This makes "Retry" implicit: as soon as the producer of
|
|
59
|
+
// the schema fixes the offending value (e.g. user edits the date field
|
|
60
|
+
// in view config), the broken widget re-mounts cleanly.
|
|
61
|
+
if (this.state.hasError &&
|
|
62
|
+
(prevProps.componentType !== this.props.componentType ||
|
|
63
|
+
prevProps.resetKey !== this.props.resetKey)) {
|
|
64
|
+
this.setState({ hasError: false, error: null });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
56
67
|
render() {
|
|
57
68
|
if (this.state.hasError && this.state.error) {
|
|
58
69
|
const error = this.state.error;
|
|
@@ -175,7 +186,7 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
175
186
|
}
|
|
176
187
|
debugTime(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
|
|
177
188
|
const renderStart = isDebug ? performance.now() : 0;
|
|
178
|
-
const rendered = (_jsx(SchemaErrorBoundary, { componentType: evaluatedSchema.type, children: React.createElement(Component, {
|
|
189
|
+
const rendered = (_jsx(SchemaErrorBoundary, { componentType: evaluatedSchema.type, resetKey: evaluatedSchema.id ?? null, children: React.createElement(Component, {
|
|
179
190
|
schema: evaluatedSchema,
|
|
180
191
|
...componentProps, // Spread non-metadata schema properties as props
|
|
181
192
|
...(evaluatedSchema.props || {}), // Override with explicit props if provided
|
|
@@ -14,6 +14,14 @@ export interface DensityConfig {
|
|
|
14
14
|
paddingClasses?: Record<DensityModeValue, string>;
|
|
15
15
|
/** Font size classes for each density mode */
|
|
16
16
|
fontSizeClasses?: Record<DensityModeValue, string>;
|
|
17
|
+
/**
|
|
18
|
+
* Optional callback fired whenever the active density mode changes.
|
|
19
|
+
* Typical use: persist the new value on the active view definition.
|
|
20
|
+
* The callback is *not* invoked for the initial mount; only on user-driven
|
|
21
|
+
* transitions (setMode / cycle) and when the controlled `initialMode`
|
|
22
|
+
* changes from upstream.
|
|
23
|
+
*/
|
|
24
|
+
onChange?: (mode: DensityModeValue) => void;
|
|
17
25
|
}
|
|
18
26
|
export interface DensityResult {
|
|
19
27
|
/** Current density mode */
|
|
@@ -33,16 +41,18 @@ export interface DensityResult {
|
|
|
33
41
|
* Hook for managing view density modes (compact/comfortable/spacious).
|
|
34
42
|
* Implements DensityMode from @objectstack/spec v2.0.7.
|
|
35
43
|
*
|
|
44
|
+
* Behaviour:
|
|
45
|
+
* - `initialMode` acts as a controlled-ish source of truth: when it changes
|
|
46
|
+
* externally (e.g. user switches to a different saved view) the internal
|
|
47
|
+
* mode follows.
|
|
48
|
+
* - User-driven transitions (`setMode` / `cycle`) emit `config.onChange`,
|
|
49
|
+
* so callers can persist the choice.
|
|
50
|
+
*
|
|
36
51
|
* @example
|
|
37
52
|
* ```tsx
|
|
38
|
-
* const density = useDensityMode('comfortable'
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* <div className={density.paddingClass}>
|
|
42
|
-
* <table style={{ rowHeight: density.rowHeight }}>...</table>
|
|
43
|
-
* <button onClick={density.cycle}>Toggle Density</button>
|
|
44
|
-
* </div>
|
|
45
|
-
* );
|
|
53
|
+
* const density = useDensityMode(activeView?.densityMode ?? 'comfortable', {
|
|
54
|
+
* onChange: (m) => dataSource.updateViewConfig(obj, vid, { densityMode: m }),
|
|
55
|
+
* });
|
|
46
56
|
* ```
|
|
47
57
|
*/
|
|
48
58
|
export declare function useDensityMode(initialMode?: DensityModeValue, config?: DensityConfig): DensityResult;
|
|
@@ -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 { useState, useCallback, useMemo } from 'react';
|
|
8
|
+
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
9
9
|
const DEFAULT_ROW_HEIGHTS = {
|
|
10
10
|
compact: 32,
|
|
11
11
|
comfortable: 40,
|
|
@@ -26,27 +26,55 @@ const DENSITY_ORDER = ['compact', 'comfortable', 'spacious'];
|
|
|
26
26
|
* Hook for managing view density modes (compact/comfortable/spacious).
|
|
27
27
|
* Implements DensityMode from @objectstack/spec v2.0.7.
|
|
28
28
|
*
|
|
29
|
+
* Behaviour:
|
|
30
|
+
* - `initialMode` acts as a controlled-ish source of truth: when it changes
|
|
31
|
+
* externally (e.g. user switches to a different saved view) the internal
|
|
32
|
+
* mode follows.
|
|
33
|
+
* - User-driven transitions (`setMode` / `cycle`) emit `config.onChange`,
|
|
34
|
+
* so callers can persist the choice.
|
|
35
|
+
*
|
|
29
36
|
* @example
|
|
30
37
|
* ```tsx
|
|
31
|
-
* const density = useDensityMode('comfortable'
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* <div className={density.paddingClass}>
|
|
35
|
-
* <table style={{ rowHeight: density.rowHeight }}>...</table>
|
|
36
|
-
* <button onClick={density.cycle}>Toggle Density</button>
|
|
37
|
-
* </div>
|
|
38
|
-
* );
|
|
38
|
+
* const density = useDensityMode(activeView?.densityMode ?? 'comfortable', {
|
|
39
|
+
* onChange: (m) => dataSource.updateViewConfig(obj, vid, { densityMode: m }),
|
|
40
|
+
* });
|
|
39
41
|
* ```
|
|
40
42
|
*/
|
|
41
43
|
export function useDensityMode(initialMode = 'comfortable', config) {
|
|
42
|
-
const [mode,
|
|
44
|
+
const [mode, setModeState] = useState(initialMode);
|
|
43
45
|
const rowHeights = config?.rowHeights ?? DEFAULT_ROW_HEIGHTS;
|
|
44
46
|
const paddingClasses = config?.paddingClasses ?? DEFAULT_PADDING_CLASSES;
|
|
45
47
|
const fontSizeClasses = config?.fontSizeClasses ?? DEFAULT_FONT_SIZE_CLASSES;
|
|
48
|
+
// Keep latest onChange in a ref so we don't need to re-create callbacks
|
|
49
|
+
// when the parent re-renders with a new function identity.
|
|
50
|
+
const onChangeRef = useRef(config?.onChange);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
onChangeRef.current = config?.onChange;
|
|
53
|
+
}, [config?.onChange]);
|
|
54
|
+
// Track the upstream initialMode so we can detect external changes
|
|
55
|
+
// (e.g. switching views) without firing onChange for those.
|
|
56
|
+
const lastInitialRef = useRef(initialMode);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (initialMode !== lastInitialRef.current) {
|
|
59
|
+
lastInitialRef.current = initialMode;
|
|
60
|
+
setModeState(initialMode);
|
|
61
|
+
}
|
|
62
|
+
}, [initialMode]);
|
|
63
|
+
const setMode = useCallback((next) => {
|
|
64
|
+
setModeState((current) => {
|
|
65
|
+
if (current === next)
|
|
66
|
+
return current;
|
|
67
|
+
// Fire onChange asynchronously so React state has settled.
|
|
68
|
+
queueMicrotask(() => onChangeRef.current?.(next));
|
|
69
|
+
return next;
|
|
70
|
+
});
|
|
71
|
+
}, []);
|
|
46
72
|
const cycle = useCallback(() => {
|
|
47
|
-
|
|
73
|
+
setModeState((current) => {
|
|
48
74
|
const idx = DENSITY_ORDER.indexOf(current);
|
|
49
|
-
|
|
75
|
+
const next = DENSITY_ORDER[(idx + 1) % DENSITY_ORDER.length];
|
|
76
|
+
queueMicrotask(() => onChangeRef.current?.(next));
|
|
77
|
+
return next;
|
|
50
78
|
});
|
|
51
79
|
}, []);
|
|
52
80
|
return useMemo(() => ({
|
|
@@ -56,5 +84,5 @@ export function useDensityMode(initialMode = 'comfortable', config) {
|
|
|
56
84
|
rowHeight: rowHeights[mode],
|
|
57
85
|
paddingClass: paddingClasses[mode],
|
|
58
86
|
fontSizeClass: fontSizeClasses[mode],
|
|
59
|
-
}), [mode, cycle, rowHeights, paddingClasses, fontSizeClasses]);
|
|
87
|
+
}), [mode, setMode, cycle, rowHeights, paddingClasses, fontSizeClasses]);
|
|
60
88
|
}
|
|
@@ -5,6 +5,23 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Normalize a schema-supplied predicate (`visible` / `enabled` / `disabled` /
|
|
10
|
+
* `hidden`) into the `${expr}` template form expected by `useCondition`.
|
|
11
|
+
*
|
|
12
|
+
* Accepts:
|
|
13
|
+
* - `boolean` → returned as-is (predicate hooks short-circuit on booleans).
|
|
14
|
+
* - `string` → wrapped as `${string}` (legacy DX shorthand).
|
|
15
|
+
* - `Expression` envelope `{ dialect, source }` (new format from
|
|
16
|
+
* `@objectstack/spec`'s normalized predicate inputs) → unwrapped, then
|
|
17
|
+
* wrapped as `${source}`. Both `cel` and `template` dialects already use
|
|
18
|
+
* compatible variable syntax (`record.x`, etc.).
|
|
19
|
+
* - `null` / `undefined` / empty → `undefined` (default visible/enabled).
|
|
20
|
+
*
|
|
21
|
+
* This is the canonical helper to use in renderers so we never end up with
|
|
22
|
+
* `${[object Object]}` after JS template-literal interpolation.
|
|
23
|
+
*/
|
|
24
|
+
export declare function toPredicateInput(value: unknown): string | boolean | undefined;
|
|
8
25
|
/**
|
|
9
26
|
* Hook for evaluating expressions with dynamic context
|
|
10
27
|
*
|
|
@@ -7,6 +7,35 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { useMemo } from 'react';
|
|
9
9
|
import { ExpressionEvaluator } from '@object-ui/core';
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a schema-supplied predicate (`visible` / `enabled` / `disabled` /
|
|
12
|
+
* `hidden`) into the `${expr}` template form expected by `useCondition`.
|
|
13
|
+
*
|
|
14
|
+
* Accepts:
|
|
15
|
+
* - `boolean` → returned as-is (predicate hooks short-circuit on booleans).
|
|
16
|
+
* - `string` → wrapped as `${string}` (legacy DX shorthand).
|
|
17
|
+
* - `Expression` envelope `{ dialect, source }` (new format from
|
|
18
|
+
* `@objectstack/spec`'s normalized predicate inputs) → unwrapped, then
|
|
19
|
+
* wrapped as `${source}`. Both `cel` and `template` dialects already use
|
|
20
|
+
* compatible variable syntax (`record.x`, etc.).
|
|
21
|
+
* - `null` / `undefined` / empty → `undefined` (default visible/enabled).
|
|
22
|
+
*
|
|
23
|
+
* This is the canonical helper to use in renderers so we never end up with
|
|
24
|
+
* `${[object Object]}` after JS template-literal interpolation.
|
|
25
|
+
*/
|
|
26
|
+
export function toPredicateInput(value) {
|
|
27
|
+
if (value === null || value === undefined || value === '')
|
|
28
|
+
return undefined;
|
|
29
|
+
if (typeof value === 'boolean')
|
|
30
|
+
return value;
|
|
31
|
+
if (typeof value === 'string')
|
|
32
|
+
return `\${${value}}`;
|
|
33
|
+
if (typeof value === 'object' && typeof value.source === 'string') {
|
|
34
|
+
const src = value.source;
|
|
35
|
+
return src ? `\${${src}}` : undefined;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
10
39
|
/**
|
|
11
40
|
* Hook for evaluating expressions with dynamic context
|
|
12
41
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/react",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.8",
|
|
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": "^4.0.
|
|
27
|
+
"@objectstack/spec": "^4.0.5",
|
|
28
28
|
"react-hook-form": "^7.75.0",
|
|
29
|
-
"@object-ui/core": "4.0.
|
|
30
|
-
"@object-ui/data-objectstack": "4.0.
|
|
31
|
-
"@object-ui/i18n": "4.0.
|
|
32
|
-
"@object-ui/types": "4.0.
|
|
29
|
+
"@object-ui/core": "4.0.8",
|
|
30
|
+
"@object-ui/data-objectstack": "4.0.8",
|
|
31
|
+
"@object-ui/i18n": "4.0.8",
|
|
32
|
+
"@object-ui/types": "4.0.8"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": "^18.0.0 || ^19.0.0",
|