@object-ui/react 4.0.7 → 4.0.9

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,24 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 4.0.9
4
+
5
+ ### Patch Changes
6
+
7
+ - @object-ui/types@4.0.9
8
+ - @object-ui/core@4.0.9
9
+ - @object-ui/i18n@4.0.9
10
+ - @object-ui/data-objectstack@4.0.9
11
+
12
+ ## 4.0.8
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [3d58eaa]
17
+ - @object-ui/i18n@4.0.8
18
+ - @object-ui/types@4.0.8
19
+ - @object-ui/core@4.0.8
20
+ - @object-ui/data-objectstack@4.0.8
21
+
3
22
  ## 4.0.7
4
23
 
5
24
  ### Patch Changes
@@ -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
  }
@@ -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
- * return (
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
- * return (
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, setMode] = useState(initialMode);
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
- setMode((current) => {
73
+ setModeState((current) => {
48
74
  const idx = DENSITY_ORDER.indexOf(current);
49
- return DENSITY_ORDER[(idx + 1) % DENSITY_ORDER.length];
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/react",
3
- "version": "4.0.7",
3
+ "version": "4.0.9",
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.4",
27
+ "@objectstack/spec": "^4.0.5",
28
28
  "react-hook-form": "^7.75.0",
29
- "@object-ui/i18n": "4.0.7",
30
- "@object-ui/core": "4.0.7",
31
- "@object-ui/data-objectstack": "4.0.7",
32
- "@object-ui/types": "4.0.7"
29
+ "@object-ui/core": "4.0.9",
30
+ "@object-ui/data-objectstack": "4.0.9",
31
+ "@object-ui/i18n": "4.0.9",
32
+ "@object-ui/types": "4.0.9"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^18.0.0 || ^19.0.0",