@object-ui/react 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +8 -0
  3. package/dist/SchemaRenderer.js +61 -2
  4. package/dist/context/SchemaRendererContext.d.ts +4 -1
  5. package/dist/context/SchemaRendererContext.js +6 -4
  6. package/dist/hooks/index.d.ts +3 -0
  7. package/dist/hooks/index.js +3 -0
  8. package/dist/hooks/useActionEngine.d.ts +27 -0
  9. package/dist/hooks/useActionEngine.js +39 -0
  10. package/dist/hooks/useDebugMode.d.ts +43 -0
  11. package/dist/hooks/useDebugMode.js +71 -0
  12. package/dist/hooks/useGlobalUndo.d.ts +35 -0
  13. package/dist/hooks/useGlobalUndo.js +86 -0
  14. package/dist/hooks/useNavigationOverlay.d.ts +2 -0
  15. package/dist/hooks/useNavigationOverlay.js +12 -5
  16. package/dist/index.d.ts +3 -1
  17. package/dist/index.js +4 -1
  18. package/dist/spec-bridge/SpecBridge.d.ts +26 -0
  19. package/dist/spec-bridge/SpecBridge.js +62 -0
  20. package/dist/spec-bridge/bridges/dashboard.d.ts +81 -0
  21. package/dist/spec-bridge/bridges/dashboard.js +121 -0
  22. package/dist/spec-bridge/bridges/form-view.d.ts +44 -0
  23. package/dist/spec-bridge/bridges/form-view.js +77 -0
  24. package/dist/spec-bridge/bridges/list-view.d.ts +82 -0
  25. package/dist/spec-bridge/bridges/list-view.js +154 -0
  26. package/dist/spec-bridge/bridges/page.d.ts +68 -0
  27. package/dist/spec-bridge/bridges/page.js +81 -0
  28. package/dist/spec-bridge/index.d.ts +13 -0
  29. package/dist/spec-bridge/index.js +12 -0
  30. package/dist/spec-bridge/types.d.ts +34 -0
  31. package/{src/index.test.ts → dist/spec-bridge/types.js} +1 -8
  32. package/package.json +7 -7
  33. package/src/SchemaRenderer.tsx +66 -0
  34. package/src/__tests__/LazyPluginLoader.test.tsx +1 -1
  35. package/src/__tests__/SchemaRenderer.debug.test.tsx +69 -0
  36. package/src/__tests__/SchemaRenderer.expressions.test.tsx +131 -0
  37. package/src/__tests__/useNavigationOverlay.test.ts +162 -0
  38. package/src/context/SchemaRendererContext.tsx +9 -4
  39. package/src/context/__tests__/useDataScope.test.tsx +91 -0
  40. package/src/hooks/__tests__/useActionEngine.test.ts +170 -0
  41. package/src/hooks/__tests__/useCrudShortcuts.test.ts +85 -0
  42. package/src/hooks/__tests__/useDebugMode.test.ts +83 -0
  43. package/src/hooks/__tests__/useFocusTrap.test.ts +247 -0
  44. package/src/hooks/__tests__/usePageTransition.test.ts +158 -0
  45. package/src/hooks/__tests__/useReducedMotion.test.ts +104 -0
  46. package/src/hooks/index.ts +3 -0
  47. package/src/hooks/useActionEngine.ts +84 -0
  48. package/src/hooks/useDebugMode.ts +95 -0
  49. package/src/hooks/useGlobalUndo.ts +98 -0
  50. package/src/hooks/useNavigationOverlay.ts +14 -5
  51. package/src/index.ts +6 -0
  52. package/src/spec-bridge/SpecBridge.ts +64 -0
  53. package/src/spec-bridge/__tests__/P1SpecBridge.test.ts +855 -0
  54. package/src/spec-bridge/__tests__/SpecBridge.test.ts +550 -0
  55. package/src/spec-bridge/bridges/dashboard.ts +180 -0
  56. package/src/spec-bridge/bridges/form-view.ts +109 -0
  57. package/src/spec-bridge/bridges/list-view.ts +187 -0
  58. package/src/spec-bridge/bridges/page.ts +136 -0
  59. package/src/spec-bridge/index.ts +14 -0
  60. package/src/spec-bridge/types.ts +38 -0
  61. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
1
1
 
2
- > @object-ui/react@3.0.2 build /home/runner/work/objectui/objectui/packages/react
2
+ > @object-ui/react@3.1.0 build /home/runner/work/objectui/objectui/packages/react
3
3
  > tsc
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 3.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - @object-ui/types@3.0.3
8
+ - @object-ui/core@3.0.3
9
+ - @object-ui/i18n@3.0.3
10
+
3
11
  ## 3.0.2
4
12
 
5
13
  ### Patch Changes
@@ -7,7 +7,7 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
7
7
  * LICENSE file in the root directory of this source tree.
8
8
  */
9
9
  import React, { forwardRef, useContext, useMemo, Component } from 'react';
10
- import { ComponentRegistry, ExpressionEvaluator, isObjectUIError, ERROR_CODES, debugLog, debugTime, debugTimeEnd, } from '@object-ui/core';
10
+ import { ComponentRegistry, ExpressionEvaluator, isObjectUIError, ERROR_CODES, debugLog, debugTime, debugTimeEnd, DebugCollector, } from '@object-ui/core';
11
11
  import { SchemaRendererContext } from './context/SchemaRendererContext';
12
12
  import { resolveI18nLabel } from './utils/i18n';
13
13
  /**
@@ -90,10 +90,45 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
90
90
  }
91
91
  newSchema.props = newProps;
92
92
  }
93
+ // Evaluate visibility: visible / visibleOn / hidden / hiddenOn
94
+ const shouldHide = (() => {
95
+ if (newSchema.visible !== undefined) {
96
+ return !evaluator.evaluateCondition(newSchema.visible);
97
+ }
98
+ if (newSchema.visibleOn !== undefined) {
99
+ return !evaluator.evaluateCondition(newSchema.visibleOn);
100
+ }
101
+ if (newSchema.hidden !== undefined) {
102
+ return evaluator.evaluateCondition(newSchema.hidden);
103
+ }
104
+ if (newSchema.hiddenOn !== undefined) {
105
+ return evaluator.evaluateCondition(newSchema.hiddenOn);
106
+ }
107
+ return false;
108
+ })();
109
+ if (shouldHide) {
110
+ newSchema._hidden = true;
111
+ }
112
+ // Evaluate disabled: disabled / disabledOn
113
+ const isDisabled = (() => {
114
+ if (newSchema.disabled !== undefined) {
115
+ return evaluator.evaluateCondition(newSchema.disabled);
116
+ }
117
+ if (newSchema.disabledOn !== undefined) {
118
+ return evaluator.evaluateCondition(newSchema.disabledOn);
119
+ }
120
+ return false;
121
+ })();
122
+ if (isDisabled) {
123
+ newSchema._disabled = true;
124
+ }
93
125
  return newSchema;
94
126
  }, [schema, dataSource]);
95
127
  if (!evaluatedSchema)
96
128
  return null;
129
+ // Handle visibility: if evaluated schema is hidden, render nothing
130
+ if (evaluatedSchema?._hidden)
131
+ return null;
97
132
  // If schema is just a string, render it as text
98
133
  if (typeof evaluatedSchema === 'string')
99
134
  return _jsx(_Fragment, { children: evaluatedSchema });
@@ -107,21 +142,45 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
107
142
  // Note: We don't forward the ref to the Component because components in the registry
108
143
  // may not support refs. The SchemaRenderer itself can still receive refs for its own use.
109
144
  // Extract schema metadata properties that should NOT be passed as React props
110
- const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleOn: _visibleOn, hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn, ...componentProps } = evaluatedSchema;
145
+ const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleOn: _visibleOn, hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn, _hidden: __hidden, // stripped: internal visibility flag
146
+ _disabled: __disabled, // stripped: internal disabled flag
147
+ ...componentProps } = evaluatedSchema;
111
148
  // Extract AriaPropsSchema properties for accessibility
112
149
  const ariaProps = resolveAriaProps(evaluatedSchema);
150
+ // Debug-mode enhancements: extra data attributes + perf tracking
151
+ const isDebug = context?.debug || context?.debugFlags?.enabled;
152
+ const debugAttrs = {};
153
+ if (isDebug) {
154
+ debugAttrs['data-debug-type'] = evaluatedSchema.type;
155
+ if (evaluatedSchema.id) {
156
+ debugAttrs['data-debug-id'] = evaluatedSchema.id;
157
+ }
158
+ }
113
159
  debugTime(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
160
+ const renderStart = isDebug ? performance.now() : 0;
114
161
  const rendered = (_jsx(SchemaErrorBoundary, { componentType: evaluatedSchema.type, children: React.createElement(Component, {
115
162
  schema: evaluatedSchema,
116
163
  ...componentProps, // Spread non-metadata schema properties as props
117
164
  ...(evaluatedSchema.props || {}), // Override with explicit props if provided
118
165
  ...ariaProps, // Inject ARIA attributes from AriaPropsSchema
166
+ ...debugAttrs, // Debug-mode data attributes
167
+ disabled: __disabled || undefined,
119
168
  className: evaluatedSchema.className,
120
169
  'data-obj-id': evaluatedSchema.id,
121
170
  'data-obj-type': evaluatedSchema.type,
122
171
  ...props
123
172
  }) }));
124
173
  debugTimeEnd(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
174
+ // Report render perf to DebugCollector when debug mode is active
175
+ if (isDebug && renderStart) {
176
+ const durationMs = performance.now() - renderStart;
177
+ DebugCollector.getInstance().addPerf({
178
+ type: evaluatedSchema.type,
179
+ id: evaluatedSchema.id,
180
+ durationMs,
181
+ timestamp: Date.now(),
182
+ });
183
+ }
125
184
  return rendered;
126
185
  });
127
186
  SchemaRenderer.displayName = 'SchemaRenderer';
@@ -1,14 +1,17 @@
1
1
  import React from 'react';
2
+ import type { DebugFlags } from '@object-ui/core';
2
3
  interface SchemaRendererContextType {
3
4
  dataSource: any;
4
5
  debug?: boolean;
6
+ debugFlags?: DebugFlags;
5
7
  }
6
8
  declare const SchemaRendererContext: React.Context<SchemaRendererContextType | null>;
7
9
  export { SchemaRendererContext };
8
- export declare const SchemaRendererProvider: ({ children, dataSource, debug }: {
10
+ export declare const SchemaRendererProvider: ({ children, dataSource, debug, debugFlags, }: {
9
11
  children: React.ReactNode;
10
12
  dataSource: any;
11
13
  debug?: boolean;
14
+ debugFlags?: DebugFlags;
12
15
  }) => import("react/jsx-runtime").JSX.Element;
13
16
  export declare const useSchemaContext: () => SchemaRendererContextType;
14
17
  export declare const useDataScope: (path?: string) => any;
@@ -2,8 +2,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useMemo } from 'react';
3
3
  const SchemaRendererContext = createContext(null);
4
4
  export { SchemaRendererContext };
5
- export const SchemaRendererProvider = ({ children, dataSource, debug }) => {
6
- const value = useMemo(() => ({ dataSource, debug }), [dataSource, debug]);
5
+ export const SchemaRendererProvider = ({ children, dataSource, debug, debugFlags, }) => {
6
+ const value = useMemo(() => ({ dataSource, debug, debugFlags }), [dataSource, debug, debugFlags]);
7
7
  return (_jsx(SchemaRendererContext.Provider, { value: value, children: children }));
8
8
  };
9
9
  export const useSchemaContext = () => {
@@ -16,8 +16,10 @@ export const useSchemaContext = () => {
16
16
  export const useDataScope = (path) => {
17
17
  const context = useContext(SchemaRendererContext);
18
18
  const dataSource = context?.dataSource;
19
- if (!dataSource || !path)
20
- return dataSource;
19
+ if (!path)
20
+ return undefined;
21
+ if (!dataSource)
22
+ return undefined;
21
23
  // Simple path resolution for now. In real app might be more complex
22
24
  return path.split('.').reduce((acc, part) => acc && acc[part], dataSource);
23
25
  };
@@ -29,3 +29,6 @@ export * from './usePageTransition';
29
29
  export * from './useViewTransition';
30
30
  export * from './useETagCache';
31
31
  export * from './useSchemaPersistence';
32
+ export * from './useGlobalUndo';
33
+ export * from './useDebugMode';
34
+ export * from './useActionEngine';
@@ -29,3 +29,6 @@ export * from './usePageTransition';
29
29
  export * from './useViewTransition';
30
30
  export * from './useETagCache';
31
31
  export * from './useSchemaPersistence';
32
+ export * from './useGlobalUndo';
33
+ export * from './useDebugMode';
34
+ export * from './useActionEngine';
@@ -0,0 +1,27 @@
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 { ActionEngine, type ActionLocation, type ActionDef, type ActionContext, type ActionResult } from '@object-ui/core';
9
+ export interface UseActionEngineOptions {
10
+ /** Action definitions to register */
11
+ actions?: ActionDef[];
12
+ /** Action context (record, user, etc.) */
13
+ context?: ActionContext;
14
+ }
15
+ export interface UseActionEngineReturn {
16
+ /** Get actions available at a specific location, sorted by priority */
17
+ getActionsForLocation: (location: ActionLocation) => ActionDef[];
18
+ /** Get actions that support bulk operations */
19
+ getBulkActions: () => ActionDef[];
20
+ /** Execute an action by name */
21
+ executeAction: (name: string, contextOverride?: Partial<ActionContext>) => Promise<ActionResult>;
22
+ /** Handle a keyboard shortcut */
23
+ handleShortcut: (keys: string) => Promise<ActionResult | null>;
24
+ /** The underlying ActionEngine instance */
25
+ engine: ActionEngine;
26
+ }
27
+ export declare function useActionEngine(options?: UseActionEngineOptions): UseActionEngineReturn;
@@ -0,0 +1,39 @@
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
+ * useActionEngine — React hook wrapping ActionEngine for location-based action management.
10
+ *
11
+ * Provides declarative access to the ActionEngine's location filtering, bulk operations,
12
+ * keyboard shortcut handling, and execution pipeline from React components.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const { getActionsForLocation, executeAction } = useActionEngine({
17
+ * actions: schema.actions,
18
+ * context: { record, user },
19
+ * });
20
+ *
21
+ * const toolbarActions = getActionsForLocation('list_toolbar');
22
+ * ```
23
+ */
24
+ import { useCallback, useMemo } from 'react';
25
+ import { ActionEngine, } from '@object-ui/core';
26
+ export function useActionEngine(options = {}) {
27
+ const { actions = [], context = {} } = options;
28
+ const engine = useMemo(() => {
29
+ const e = new ActionEngine(context);
30
+ e.registerActions(actions);
31
+ return e;
32
+ // eslint-disable-next-line react-hooks/exhaustive-deps
33
+ }, [JSON.stringify(actions), JSON.stringify(context)]);
34
+ const getActionsForLocation = useCallback((location) => engine.getActionsForLocation(location), [engine]);
35
+ const getBulkActions = useCallback(() => engine.getBulkActions(), [engine]);
36
+ const executeAction = useCallback((name, contextOverride) => engine.executeAction(name, contextOverride), [engine]);
37
+ const handleShortcut = useCallback((keys) => engine.handleShortcut(keys), [engine]);
38
+ return { getActionsForLocation, getBulkActions, executeAction, handleShortcut, engine };
39
+ }
@@ -0,0 +1,43 @@
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 DebugFlags } from '@object-ui/core';
9
+ /**
10
+ * Result returned by {@link useDebugMode}.
11
+ */
12
+ export interface UseDebugModeResult {
13
+ /** Whether debug mode is currently enabled */
14
+ enabled: boolean;
15
+ /** Fine-grained debug flags parsed from the URL */
16
+ flags: DebugFlags;
17
+ /** Toggle debug mode on/off */
18
+ toggle: () => void;
19
+ /** Explicitly set debug mode */
20
+ setEnabled: (value: boolean) => void;
21
+ }
22
+ /**
23
+ * Hook for managing debug mode.
24
+ *
25
+ * Automatically detects `?__debug` (and sub-flags like `?__debug_schema`)
26
+ * from the URL. Supports manual toggle and Ctrl+Shift+D keyboard shortcut.
27
+ *
28
+ * SSR-safe — returns disabled state when `window` is unavailable.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * function App() {
33
+ * const debug = useDebugMode();
34
+ * return (
35
+ * <>
36
+ * {debug.enabled && <DebugPanel flags={debug.flags} />}
37
+ * <button onClick={debug.toggle}>Toggle Debug</button>
38
+ * </>
39
+ * );
40
+ * }
41
+ * ```
42
+ */
43
+ export declare function useDebugMode(): UseDebugModeResult;
@@ -0,0 +1,71 @@
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 { useState, useEffect, useCallback, useMemo } from 'react';
9
+ import { parseDebugFlags } from '@object-ui/core';
10
+ /**
11
+ * Hook for managing debug mode.
12
+ *
13
+ * Automatically detects `?__debug` (and sub-flags like `?__debug_schema`)
14
+ * from the URL. Supports manual toggle and Ctrl+Shift+D keyboard shortcut.
15
+ *
16
+ * SSR-safe — returns disabled state when `window` is unavailable.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * function App() {
21
+ * const debug = useDebugMode();
22
+ * return (
23
+ * <>
24
+ * {debug.enabled && <DebugPanel flags={debug.flags} />}
25
+ * <button onClick={debug.toggle}>Toggle Debug</button>
26
+ * </>
27
+ * );
28
+ * }
29
+ * ```
30
+ */
31
+ export function useDebugMode() {
32
+ const urlFlags = useMemo(() => {
33
+ try {
34
+ return typeof window !== 'undefined'
35
+ ? parseDebugFlags(window.location.search)
36
+ : { enabled: false };
37
+ }
38
+ catch {
39
+ return { enabled: false };
40
+ }
41
+ }, []);
42
+ const [manualEnabled, setManualEnabled] = useState(null);
43
+ // Effective enabled: manual override wins, then URL flags
44
+ const enabled = manualEnabled !== null ? manualEnabled : urlFlags.enabled;
45
+ // Flags reflect URL-parsed flags plus effective enabled state
46
+ const flags = useMemo(() => ({ ...urlFlags, enabled }), [urlFlags, enabled]);
47
+ const toggle = useCallback(() => {
48
+ setManualEnabled((prev) => {
49
+ if (prev === null)
50
+ return !urlFlags.enabled;
51
+ return !prev;
52
+ });
53
+ }, [urlFlags.enabled]);
54
+ const setEnabled = useCallback((value) => {
55
+ setManualEnabled(value);
56
+ }, []);
57
+ // Ctrl+Shift+D keyboard shortcut
58
+ useEffect(() => {
59
+ if (typeof window === 'undefined')
60
+ return;
61
+ const handler = (e) => {
62
+ if (e.ctrlKey && e.shiftKey && e.key === 'D') {
63
+ e.preventDefault();
64
+ toggle();
65
+ }
66
+ };
67
+ document.addEventListener('keydown', handler);
68
+ return () => document.removeEventListener('keydown', handler);
69
+ }, [toggle]);
70
+ return { enabled, flags, toggle, setEnabled };
71
+ }
@@ -0,0 +1,35 @@
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 UndoableOperation } from '@object-ui/core';
9
+ export interface UseGlobalUndoOptions {
10
+ /** DataSource to execute undo/redo operations. */
11
+ dataSource?: {
12
+ create(objectName: string, data: Record<string, unknown>): Promise<unknown>;
13
+ update(objectName: string, recordId: string, data: Record<string, unknown>): Promise<unknown>;
14
+ delete(objectName: string, recordId: string): Promise<unknown>;
15
+ };
16
+ /** Callback after successful undo. */
17
+ onUndo?: (op: UndoableOperation) => void;
18
+ /** Callback after successful redo. */
19
+ onRedo?: (op: UndoableOperation) => void;
20
+ }
21
+ /**
22
+ * React hook that wraps the global UndoManager for use in console components.
23
+ *
24
+ * Provides reactive undo/redo state, executes data operations through the
25
+ * supplied dataSource, and registers Ctrl+Z / Ctrl+Shift+Z keyboard shortcuts.
26
+ */
27
+ export declare function useGlobalUndo(options?: UseGlobalUndoOptions): {
28
+ undo: () => Promise<void>;
29
+ redo: () => Promise<void>;
30
+ canUndo: boolean;
31
+ canRedo: boolean;
32
+ undoDescription: string | undefined;
33
+ redoDescription: string | undefined;
34
+ history: UndoableOperation[];
35
+ };
@@ -0,0 +1,86 @@
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 { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
9
+ import { globalUndoManager } from '@object-ui/core';
10
+ function getSnapshot() {
11
+ return {
12
+ canUndo: globalUndoManager.canUndo,
13
+ canRedo: globalUndoManager.canRedo,
14
+ undoDescription: globalUndoManager.peekUndo()?.description,
15
+ redoDescription: globalUndoManager.peekRedo()?.description,
16
+ history: globalUndoManager.getHistory(),
17
+ };
18
+ }
19
+ function getServerSnapshot() {
20
+ return { canUndo: false, canRedo: false, undoDescription: undefined, redoDescription: undefined, history: [] };
21
+ }
22
+ // Cache reference to avoid re-renders when nothing changed
23
+ let cachedSnapshot = getSnapshot();
24
+ function subscribe(callback) {
25
+ return globalUndoManager.subscribe(() => {
26
+ cachedSnapshot = getSnapshot();
27
+ callback();
28
+ });
29
+ }
30
+ function getCachedSnapshot() { return cachedSnapshot; }
31
+ /**
32
+ * React hook that wraps the global UndoManager for use in console components.
33
+ *
34
+ * Provides reactive undo/redo state, executes data operations through the
35
+ * supplied dataSource, and registers Ctrl+Z / Ctrl+Shift+Z keyboard shortcuts.
36
+ */
37
+ export function useGlobalUndo(options = {}) {
38
+ const optionsRef = useRef(options);
39
+ optionsRef.current = options;
40
+ const state = useSyncExternalStore(subscribe, getCachedSnapshot, getServerSnapshot);
41
+ const executeOp = useCallback(async (op, data, mode) => {
42
+ const ds = optionsRef.current.dataSource;
43
+ if (!ds)
44
+ return;
45
+ const action = mode === 'undo'
46
+ ? { create: 'delete', update: 'update', delete: 'create' }[op.type]
47
+ : op.type;
48
+ if (action === 'delete')
49
+ await ds.delete(op.objectName, op.recordId);
50
+ else if (action === 'update')
51
+ await ds.update(op.objectName, op.recordId, data);
52
+ else
53
+ await ds.create(op.objectName, data);
54
+ }, []);
55
+ const undo = useCallback(async () => {
56
+ const op = globalUndoManager.popUndo();
57
+ if (!op)
58
+ return;
59
+ await executeOp(op, op.undoData, 'undo');
60
+ optionsRef.current.onUndo?.(op);
61
+ }, [executeOp]);
62
+ const redo = useCallback(async () => {
63
+ const op = globalUndoManager.popRedo();
64
+ if (!op)
65
+ return;
66
+ await executeOp(op, op.redoData, 'redo');
67
+ optionsRef.current.onRedo?.(op);
68
+ }, [executeOp]);
69
+ // Keyboard shortcuts: Ctrl+Z (undo), Ctrl+Shift+Z (redo)
70
+ useEffect(() => {
71
+ const handler = (e) => {
72
+ if (!(e.ctrlKey || e.metaKey) || e.key.toLowerCase() !== 'z')
73
+ return;
74
+ e.preventDefault();
75
+ if (e.shiftKey) {
76
+ void redo();
77
+ }
78
+ else {
79
+ void undo();
80
+ }
81
+ };
82
+ window.addEventListener('keydown', handler);
83
+ return () => window.removeEventListener('keydown', handler);
84
+ }, [undo, redo]);
85
+ return useMemo(() => ({ ...state, undo, redo }), [state, undo, redo]);
86
+ }
@@ -47,6 +47,8 @@ export interface NavigationOverlayState {
47
47
  width: string | number | undefined;
48
48
  /** Whether navigation is an overlay mode (drawer/modal/split/popover) */
49
49
  isOverlay: boolean;
50
+ /** The target view/form name from NavigationConfig */
51
+ view: string | undefined;
50
52
  }
51
53
  /**
52
54
  * Hook for NavigationConfig-driven navigation overlay.
@@ -49,6 +49,7 @@ export function useNavigationOverlay(options) {
49
49
  const [selectedRecord, setSelectedRecord] = useState(null);
50
50
  const mode = navigation?.mode ?? 'page';
51
51
  const width = navigation?.width;
52
+ const view = navigation?.view;
52
53
  const isOverlay = mode === 'drawer' || mode === 'modal' || mode === 'split' || mode === 'popover';
53
54
  const close = useCallback(() => {
54
55
  setIsOpen(false);
@@ -68,7 +69,7 @@ export function useNavigationOverlay(options) {
68
69
  if (!navigation) {
69
70
  const recordId = record._id || record.id;
70
71
  if (onNavigate && recordId != null) {
71
- onNavigate(recordId, 'view');
72
+ onNavigate(recordId, view ?? 'view');
72
73
  }
73
74
  return;
74
75
  }
@@ -76,10 +77,15 @@ export function useNavigationOverlay(options) {
76
77
  if (mode === 'none' || navigation.preventNavigation) {
77
78
  return;
78
79
  }
79
- // new_window / openNewTab — open in new browser tab
80
+ // new_window / openNewTab — delegate to onNavigate when available, else open directly
80
81
  if (mode === 'new_window' || navigation.openNewTab) {
81
82
  const recordId = record._id || record.id;
82
- const url = objectName ? `/${objectName}/${recordId}` : `/${recordId}`;
83
+ if (onNavigate && recordId != null) {
84
+ onNavigate(recordId, 'new_window');
85
+ return;
86
+ }
87
+ const viewPath = view ? `/${view}` : '';
88
+ const url = objectName ? `/${objectName}/${recordId}${viewPath}` : `/${recordId}${viewPath}`;
83
89
  window.open(url, '_blank');
84
90
  return;
85
91
  }
@@ -87,7 +93,7 @@ export function useNavigationOverlay(options) {
87
93
  if (mode === 'page') {
88
94
  const recordId = record._id || record.id;
89
95
  if (onNavigate && recordId != null) {
90
- onNavigate(recordId, 'view');
96
+ onNavigate(recordId, view ?? 'view');
91
97
  }
92
98
  return;
93
99
  }
@@ -107,6 +113,7 @@ export function useNavigationOverlay(options) {
107
113
  setIsOpen,
108
114
  handleClick,
109
115
  width,
116
+ view,
110
117
  isOverlay,
111
- }), [isOpen, selectedRecord, mode, close, open, handleClick, width, isOverlay]);
118
+ }), [isOpen, selectedRecord, mode, close, open, handleClick, width, view, isOverlay]);
112
119
  }
package/dist/index.d.ts CHANGED
@@ -10,4 +10,6 @@ export * from './hooks';
10
10
  export * from './context';
11
11
  export * from './components/form';
12
12
  export * from './LazyPluginLoader';
13
- export { I18nProvider, useObjectTranslation, 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';
13
+ export * from './spec-bridge';
14
+ export { resolveI18nLabel } from './utils/i18n';
15
+ export { I18nProvider, useObjectTranslation, 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
@@ -10,5 +10,8 @@ export * from './hooks'; // will be empty for now
10
10
  export * from './context'; // will be empty for now
11
11
  export * from './components/form';
12
12
  export * from './LazyPluginLoader';
13
+ export * from './spec-bridge';
14
+ // i18n utilities
15
+ export { resolveI18nLabel } from './utils/i18n';
13
16
  // Built-in i18n support
14
- export { I18nProvider, useObjectTranslation, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from '@object-ui/i18n';
17
+ export { I18nProvider, useObjectTranslation, useObjectLabel, useSafeFieldLabel, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from '@object-ui/i18n';
@@ -0,0 +1,26 @@
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 { SchemaNode } from '@object-ui/core';
9
+ import type { BridgeContext, BridgeFn } from './types';
10
+ export declare class SpecBridge {
11
+ private bridges;
12
+ private context;
13
+ constructor(context?: BridgeContext);
14
+ register(specType: string, bridge: BridgeFn): void;
15
+ /** Transform a spec schema into a SchemaNode tree */
16
+ transform(specType: string, spec: any): SchemaNode;
17
+ /** Transform a ListView spec */
18
+ transformListView(spec: any): SchemaNode;
19
+ /** Transform a FormView spec */
20
+ transformFormView(spec: any): SchemaNode;
21
+ /** Transform a Page spec */
22
+ transformPage(spec: any): SchemaNode;
23
+ /** Transform a Dashboard spec */
24
+ transformDashboard(spec: any): SchemaNode;
25
+ updateContext(ctx: Partial<BridgeContext>): void;
26
+ }
@@ -0,0 +1,62 @@
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 { bridgeListView } from './bridges/list-view';
9
+ import { bridgeFormView } from './bridges/form-view';
10
+ import { bridgePage } from './bridges/page';
11
+ import { bridgeDashboard } from './bridges/dashboard';
12
+ export class SpecBridge {
13
+ constructor(context = {}) {
14
+ Object.defineProperty(this, "bridges", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: new Map()
19
+ });
20
+ Object.defineProperty(this, "context", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: void 0
25
+ });
26
+ this.context = context;
27
+ this.register('list', bridgeListView);
28
+ this.register('form', bridgeFormView);
29
+ this.register('page', bridgePage);
30
+ this.register('dashboard', bridgeDashboard);
31
+ }
32
+ register(specType, bridge) {
33
+ this.bridges.set(specType, bridge);
34
+ }
35
+ /** Transform a spec schema into a SchemaNode tree */
36
+ transform(specType, spec) {
37
+ const bridge = this.bridges.get(specType);
38
+ if (!bridge) {
39
+ throw new Error(`No bridge registered for spec type: ${specType}`);
40
+ }
41
+ return bridge(spec, this.context);
42
+ }
43
+ /** Transform a ListView spec */
44
+ transformListView(spec) {
45
+ return this.transform('list', spec);
46
+ }
47
+ /** Transform a FormView spec */
48
+ transformFormView(spec) {
49
+ return this.transform('form', spec);
50
+ }
51
+ /** Transform a Page spec */
52
+ transformPage(spec) {
53
+ return this.transform('page', spec);
54
+ }
55
+ /** Transform a Dashboard spec */
56
+ transformDashboard(spec) {
57
+ return this.transform('dashboard', spec);
58
+ }
59
+ updateContext(ctx) {
60
+ this.context = { ...this.context, ...ctx };
61
+ }
62
+ }