@object-ui/react 3.0.3 → 3.1.1
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +9 -0
- package/dist/SchemaRenderer.js +61 -2
- package/dist/context/SchemaRendererContext.d.ts +4 -1
- package/dist/context/SchemaRendererContext.js +6 -4
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/useActionEngine.d.ts +27 -0
- package/dist/hooks/useActionEngine.js +39 -0
- package/dist/hooks/useDebugMode.d.ts +43 -0
- package/dist/hooks/useDebugMode.js +71 -0
- package/dist/hooks/useGlobalUndo.d.ts +35 -0
- package/dist/hooks/useGlobalUndo.js +86 -0
- package/dist/hooks/useNavigationOverlay.d.ts +2 -0
- package/dist/hooks/useNavigationOverlay.js +12 -5
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/spec-bridge/SpecBridge.d.ts +26 -0
- package/dist/spec-bridge/SpecBridge.js +62 -0
- package/dist/spec-bridge/bridges/dashboard.d.ts +81 -0
- package/dist/spec-bridge/bridges/dashboard.js +121 -0
- package/dist/spec-bridge/bridges/form-view.d.ts +44 -0
- package/dist/spec-bridge/bridges/form-view.js +77 -0
- package/dist/spec-bridge/bridges/list-view.d.ts +82 -0
- package/dist/spec-bridge/bridges/list-view.js +154 -0
- package/dist/spec-bridge/bridges/page.d.ts +68 -0
- package/dist/spec-bridge/bridges/page.js +81 -0
- package/dist/spec-bridge/index.d.ts +13 -0
- package/dist/spec-bridge/index.js +12 -0
- package/dist/spec-bridge/types.d.ts +34 -0
- package/dist/spec-bridge/types.js +8 -0
- package/package.json +9 -8
- package/src/SchemaRenderer.tsx +66 -0
- package/src/__tests__/LazyPluginLoader.test.tsx +1 -1
- package/src/__tests__/SchemaRenderer.debug.test.tsx +69 -0
- package/src/__tests__/SchemaRenderer.expressions.test.tsx +131 -0
- package/src/__tests__/useNavigationOverlay.test.ts +162 -0
- package/src/context/SchemaRendererContext.tsx +9 -4
- package/src/context/__tests__/useDataScope.test.tsx +91 -0
- package/src/hooks/__tests__/useActionEngine.test.ts +170 -0
- package/src/hooks/__tests__/useCrudShortcuts.test.ts +85 -0
- package/src/hooks/__tests__/useDebugMode.test.ts +83 -0
- package/src/hooks/__tests__/useFocusTrap.test.ts +247 -0
- package/src/hooks/__tests__/usePageTransition.test.ts +158 -0
- package/src/hooks/__tests__/useReducedMotion.test.ts +104 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useActionEngine.ts +84 -0
- package/src/hooks/useDebugMode.ts +95 -0
- package/src/hooks/useGlobalUndo.ts +98 -0
- package/src/hooks/useNavigationOverlay.ts +14 -5
- package/src/index.ts +6 -0
- package/src/spec-bridge/SpecBridge.ts +64 -0
- package/src/spec-bridge/__tests__/P1SpecBridge.test.ts +855 -0
- package/src/spec-bridge/__tests__/SpecBridge.test.ts +550 -0
- package/src/spec-bridge/bridges/dashboard.ts +180 -0
- package/src/spec-bridge/bridges/form-view.ts +109 -0
- package/src/spec-bridge/bridges/list-view.ts +187 -0
- package/src/spec-bridge/bridges/page.ts +136 -0
- package/src/spec-bridge/index.ts +14 -0
- package/src/spec-bridge/types.ts +38 -0
- package/tsconfig.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
package/dist/SchemaRenderer.js
CHANGED
|
@@ -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,
|
|
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 (!
|
|
20
|
-
return
|
|
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
|
};
|
package/dist/hooks/index.d.ts
CHANGED
package/dist/hooks/index.js
CHANGED
|
@@ -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 —
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|