@object-ui/react 4.3.0 → 4.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 +30 -0
- package/dist/SchemaRenderer.js +20 -2
- package/dist/context/RecordContext.d.ts +44 -0
- package/dist/context/RecordContext.js +41 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/usePageAssignment.d.ts +44 -0
- package/dist/hooks/usePageAssignment.js +91 -0
- package/dist/spec-bridge/bridges/list-view.d.ts +3 -0
- package/dist/spec-bridge/bridges/list-view.js +4 -0
- package/dist/spec-bridge/bridges/page.js +11 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @object-ui/react
|
|
2
2
|
|
|
3
|
+
## 4.5.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [ab5e281]
|
|
8
|
+
- Updated dependencies [22fa558]
|
|
9
|
+
- @object-ui/types@4.5.0
|
|
10
|
+
- @object-ui/i18n@4.5.0
|
|
11
|
+
- @object-ui/core@4.5.0
|
|
12
|
+
- @object-ui/data-objectstack@4.5.0
|
|
13
|
+
|
|
14
|
+
## 4.4.0
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- @object-ui/types@4.4.0
|
|
19
|
+
- @object-ui/core@4.4.0
|
|
20
|
+
- @object-ui/i18n@4.4.0
|
|
21
|
+
- @object-ui/data-objectstack@4.4.0
|
|
22
|
+
|
|
23
|
+
## 4.3.1
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [5f4ac6e]
|
|
28
|
+
- @object-ui/i18n@4.3.1
|
|
29
|
+
- @object-ui/types@4.3.1
|
|
30
|
+
- @object-ui/core@4.3.1
|
|
31
|
+
- @object-ui/data-objectstack@4.3.1
|
|
32
|
+
|
|
3
33
|
## 4.3.0
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
package/dist/SchemaRenderer.js
CHANGED
|
@@ -90,9 +90,27 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
90
90
|
// Shallow copy
|
|
91
91
|
const newSchema = { ...schema };
|
|
92
92
|
// COMPAT: Hoist 'properties' up to schema level
|
|
93
|
-
// This allows support for strict configs that wrap all props in 'properties'
|
|
93
|
+
// This allows support for strict configs that wrap all props in 'properties'.
|
|
94
|
+
// IMPORTANT: never let inner `properties.type` / `properties.id` shadow the
|
|
95
|
+
// outer component descriptor — those identify which renderer to dispatch to
|
|
96
|
+
// (e.g. 'page:tabs'), whereas inner `type` may be a renderer-specific prop
|
|
97
|
+
// (e.g. tab visual style: 'line' | 'card' | 'pill'). Keep `properties`
|
|
98
|
+
// intact on the schema so renderers can still read these collision-prone
|
|
99
|
+
// keys via `schema.properties.<key>`.
|
|
94
100
|
if (newSchema.properties) {
|
|
95
|
-
|
|
101
|
+
const outerType = newSchema.type;
|
|
102
|
+
const outerId = newSchema.id;
|
|
103
|
+
const props = newSchema.properties;
|
|
104
|
+
for (const [k, v] of Object.entries(props)) {
|
|
105
|
+
if (k === 'type' || k === 'id')
|
|
106
|
+
continue;
|
|
107
|
+
newSchema[k] = v;
|
|
108
|
+
}
|
|
109
|
+
if (outerType !== undefined)
|
|
110
|
+
newSchema.type = outerType;
|
|
111
|
+
if (outerId !== undefined)
|
|
112
|
+
newSchema.id = outerId;
|
|
113
|
+
newSchema.properties = props;
|
|
96
114
|
}
|
|
97
115
|
// Evaluate 'content' (common in Text, Button)
|
|
98
116
|
if (typeof newSchema.content === 'string') {
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
* RecordContext — shared record state for `record:*` namespace renderers
|
|
9
|
+
* inside a `PageSchema` (type='record'). The page host (e.g. RecordDetailView)
|
|
10
|
+
* mounts <RecordContextProvider> once; each `record:details / record:related_list
|
|
11
|
+
* / record:highlights / record:activity / record:chatter / record:path` renderer
|
|
12
|
+
* inside the page consumes data via `useRecordContext()` instead of having to
|
|
13
|
+
* receive props from the schema tree.
|
|
14
|
+
*/
|
|
15
|
+
import React from 'react';
|
|
16
|
+
export interface RecordContextValue<TData = any, TObjectSchema = any> {
|
|
17
|
+
/** Object machine name, e.g. "crm_opportunity". */
|
|
18
|
+
objectName: string;
|
|
19
|
+
/** Primary key value of the record being displayed. */
|
|
20
|
+
recordId: string | number | null | undefined;
|
|
21
|
+
/** Optional datasource id; mirrors the page-level datasource override. */
|
|
22
|
+
dataSource?: string;
|
|
23
|
+
/** Loaded record data (flat record map). Undefined while loading. */
|
|
24
|
+
data?: TData;
|
|
25
|
+
/** Resolved object metadata schema (fields, label, etc.). */
|
|
26
|
+
objectSchema?: TObjectSchema;
|
|
27
|
+
/** True while the record is fetching. */
|
|
28
|
+
loading?: boolean;
|
|
29
|
+
/** Last fetch error, if any. */
|
|
30
|
+
error?: Error | null;
|
|
31
|
+
/** Re-fetch the record from the source. */
|
|
32
|
+
refresh?: () => void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface RecordContextProviderProps extends RecordContextValue {
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
}
|
|
37
|
+
export declare const RecordContextProvider: React.FC<RecordContextProviderProps>;
|
|
38
|
+
/**
|
|
39
|
+
* Read the current record context. Returns `null` when called outside a
|
|
40
|
+
* <RecordContextProvider> — record:* renderers should treat that as "no
|
|
41
|
+
* record bound" rather than throwing, so they can still render statically
|
|
42
|
+
* inside the Studio designer/palette.
|
|
43
|
+
*/
|
|
44
|
+
export declare function useRecordContext<TData = any, TObjectSchema = any>(): RecordContextValue<TData, TObjectSchema> | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ObjectUI
|
|
4
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* RecordContext — shared record state for `record:*` namespace renderers
|
|
10
|
+
* inside a `PageSchema` (type='record'). The page host (e.g. RecordDetailView)
|
|
11
|
+
* mounts <RecordContextProvider> once; each `record:details / record:related_list
|
|
12
|
+
* / record:highlights / record:activity / record:chatter / record:path` renderer
|
|
13
|
+
* inside the page consumes data via `useRecordContext()` instead of having to
|
|
14
|
+
* receive props from the schema tree.
|
|
15
|
+
*/
|
|
16
|
+
import React from 'react';
|
|
17
|
+
const RecordContext = React.createContext(null);
|
|
18
|
+
export const RecordContextProvider = ({ children, ...value }) => {
|
|
19
|
+
// Memoize so consumers that rely on referential equality don't re-render
|
|
20
|
+
// on unrelated parent renders.
|
|
21
|
+
const memo = React.useMemo(() => value, [
|
|
22
|
+
value.objectName,
|
|
23
|
+
value.recordId,
|
|
24
|
+
value.dataSource,
|
|
25
|
+
value.data,
|
|
26
|
+
value.objectSchema,
|
|
27
|
+
value.loading,
|
|
28
|
+
value.error,
|
|
29
|
+
value.refresh,
|
|
30
|
+
]);
|
|
31
|
+
return _jsx(RecordContext.Provider, { value: memo, children: children });
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Read the current record context. Returns `null` when called outside a
|
|
35
|
+
* <RecordContextProvider> — record:* renderers should treat that as "no
|
|
36
|
+
* record bound" rather than throwing, so they can still render statically
|
|
37
|
+
* inside the Studio designer/palette.
|
|
38
|
+
*/
|
|
39
|
+
export function useRecordContext() {
|
|
40
|
+
return React.useContext(RecordContext);
|
|
41
|
+
}
|
package/dist/context/index.d.ts
CHANGED
package/dist/context/index.js
CHANGED
package/dist/hooks/index.d.ts
CHANGED
package/dist/hooks/index.js
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
* `usePageAssignment` — resolve the record PageSchema that should be rendered
|
|
9
|
+
* for a given object. Walks the metadata cache exposed by `<MetadataProvider>`
|
|
10
|
+
* and returns the first PageSchema whose `pageType === 'record'` and `object`
|
|
11
|
+
* matches the requested name.
|
|
12
|
+
*
|
|
13
|
+
* Future work (deferred): recordType / profile / app / formFactor filtering
|
|
14
|
+
* and priority-based selection. For now we return the first match so that
|
|
15
|
+
* callers can deterministically fall back to the auto-generated DetailView
|
|
16
|
+
* when no record Page is authored.
|
|
17
|
+
*/
|
|
18
|
+
export interface PageAssignmentOptions {
|
|
19
|
+
/** Salesforce-style record type filter (reserved for future use). */
|
|
20
|
+
recordType?: string;
|
|
21
|
+
/** User profile filter (reserved for future use). */
|
|
22
|
+
profile?: string;
|
|
23
|
+
/** Owning app filter (reserved for future use). */
|
|
24
|
+
app?: string;
|
|
25
|
+
/** Form factor filter (reserved for future use). */
|
|
26
|
+
formFactor?: 'desktop' | 'tablet' | 'phone';
|
|
27
|
+
/** Optional explicit page name override; bypasses object-based lookup. */
|
|
28
|
+
pageName?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface PageAssignmentResult {
|
|
31
|
+
/** Resolved PageSchema, or null if no record Page is available. */
|
|
32
|
+
page: any | null;
|
|
33
|
+
/** True while the metadata cache is still loading the `page` type. */
|
|
34
|
+
loading: boolean;
|
|
35
|
+
/** Loader error, if any. */
|
|
36
|
+
error: Error | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the record PageSchema for the given object name. Returns `null`
|
|
40
|
+
* when no record Page is configured, signalling the caller to fall back
|
|
41
|
+
* to the auto-generated detail view.
|
|
42
|
+
*/
|
|
43
|
+
export declare function usePageAssignment(objectName: string | undefined | null, opts?: PageAssignmentOptions): PageAssignmentResult;
|
|
44
|
+
export default usePageAssignment;
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
* `usePageAssignment` — resolve the record PageSchema that should be rendered
|
|
9
|
+
* for a given object. Walks the metadata cache exposed by `<MetadataProvider>`
|
|
10
|
+
* and returns the first PageSchema whose `pageType === 'record'` and `object`
|
|
11
|
+
* matches the requested name.
|
|
12
|
+
*
|
|
13
|
+
* Future work (deferred): recordType / profile / app / formFactor filtering
|
|
14
|
+
* and priority-based selection. For now we return the first match so that
|
|
15
|
+
* callers can deterministically fall back to the auto-generated DetailView
|
|
16
|
+
* when no record Page is authored.
|
|
17
|
+
*/
|
|
18
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
19
|
+
import { useMetadata } from '../context/AppShellContext';
|
|
20
|
+
function matchesAssignment(_page, _opts) {
|
|
21
|
+
// Placeholder — every page matches until rules-based assignment lands.
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the record PageSchema for the given object name. Returns `null`
|
|
26
|
+
* when no record Page is configured, signalling the caller to fall back
|
|
27
|
+
* to the auto-generated detail view.
|
|
28
|
+
*/
|
|
29
|
+
export function usePageAssignment(objectName, opts = {}) {
|
|
30
|
+
const meta = useMetadata();
|
|
31
|
+
const [ensured, setEnsured] = useState(false);
|
|
32
|
+
const [ensureError, setEnsureError] = useState(null);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
let cancelled = false;
|
|
35
|
+
if (!meta.ensureType) {
|
|
36
|
+
setEnsured(true);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
meta
|
|
40
|
+
.ensureType('page')
|
|
41
|
+
.then(() => {
|
|
42
|
+
if (!cancelled) {
|
|
43
|
+
setEnsured(true);
|
|
44
|
+
setEnsureError(null);
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.catch(err => {
|
|
48
|
+
if (!cancelled) {
|
|
49
|
+
setEnsureError(err instanceof Error ? err : new Error(String(err)));
|
|
50
|
+
setEnsured(true);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return () => {
|
|
54
|
+
cancelled = true;
|
|
55
|
+
};
|
|
56
|
+
}, [meta]);
|
|
57
|
+
const page = useMemo(() => {
|
|
58
|
+
if (!objectName && !opts.pageName)
|
|
59
|
+
return null;
|
|
60
|
+
const pages = Array.isArray(meta.pages) ? meta.pages : [];
|
|
61
|
+
if (!pages.length)
|
|
62
|
+
return null;
|
|
63
|
+
// Explicit page name override wins.
|
|
64
|
+
if (opts.pageName) {
|
|
65
|
+
return pages.find(p => p?.name === opts.pageName) ?? null;
|
|
66
|
+
}
|
|
67
|
+
const candidates = pages.filter(p => {
|
|
68
|
+
if (!p)
|
|
69
|
+
return false;
|
|
70
|
+
const pageType = p.pageType ?? p.type === 'record' ? p.pageType : p.pageType;
|
|
71
|
+
const isRecord = (p.pageType ?? (p.type === 'record' ? 'record' : undefined)) === 'record';
|
|
72
|
+
if (!isRecord)
|
|
73
|
+
return false;
|
|
74
|
+
if (p.object !== objectName)
|
|
75
|
+
return false;
|
|
76
|
+
return matchesAssignment(p, opts);
|
|
77
|
+
});
|
|
78
|
+
if (!candidates.length)
|
|
79
|
+
return null;
|
|
80
|
+
// Stable ordering: respect explicit `priority` if present (higher wins),
|
|
81
|
+
// otherwise fall back to declaration order.
|
|
82
|
+
candidates.sort((a, b) => (b?.priority ?? 0) - (a?.priority ?? 0));
|
|
83
|
+
return candidates[0];
|
|
84
|
+
}, [meta.pages, objectName, opts.pageName, opts.recordType, opts.profile, opts.app, opts.formFactor]);
|
|
85
|
+
return {
|
|
86
|
+
page,
|
|
87
|
+
loading: meta.loading || !ensured,
|
|
88
|
+
error: ensureError ?? meta.error ?? null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export default usePageAssignment;
|
|
@@ -49,6 +49,7 @@ interface ListViewSpec {
|
|
|
49
49
|
timeline?: ListViewTimelineConfig;
|
|
50
50
|
rowActions?: string[];
|
|
51
51
|
bulkActions?: string[];
|
|
52
|
+
bulkActionDefs?: any[];
|
|
52
53
|
virtualScroll?: boolean;
|
|
53
54
|
conditionalFormatting?: Array<{
|
|
54
55
|
condition: string;
|
|
@@ -63,6 +64,8 @@ interface ListViewSpec {
|
|
|
63
64
|
};
|
|
64
65
|
userActions?: any;
|
|
65
66
|
appearance?: any;
|
|
67
|
+
/** UX-only hint: collapse appearance/grouping cluster into a single popover. */
|
|
68
|
+
compactToolbar?: boolean;
|
|
66
69
|
tabs?: any[];
|
|
67
70
|
addRecord?: any;
|
|
68
71
|
showRecordCount?: boolean;
|
|
@@ -108,6 +108,8 @@ export const bridgeListView = (spec, _context) => {
|
|
|
108
108
|
node.rowActions = spec.rowActions;
|
|
109
109
|
if (spec.bulkActions)
|
|
110
110
|
node.bulkActions = spec.bulkActions;
|
|
111
|
+
if (spec.bulkActionDefs)
|
|
112
|
+
node.bulkActionDefs = spec.bulkActionDefs;
|
|
111
113
|
if (spec.virtualScroll != null)
|
|
112
114
|
node.virtualScroll = spec.virtualScroll;
|
|
113
115
|
if (spec.conditionalFormatting)
|
|
@@ -122,6 +124,8 @@ export const bridgeListView = (spec, _context) => {
|
|
|
122
124
|
node.userActions = spec.userActions;
|
|
123
125
|
if (spec.appearance)
|
|
124
126
|
node.appearance = spec.appearance;
|
|
127
|
+
if (spec.compactToolbar != null)
|
|
128
|
+
node.compactToolbar = spec.compactToolbar;
|
|
125
129
|
if (spec.tabs)
|
|
126
130
|
node.tabs = spec.tabs;
|
|
127
131
|
if (spec.addRecord)
|
|
@@ -15,7 +15,17 @@ function mapComponent(component) {
|
|
|
15
15
|
if (component.className)
|
|
16
16
|
node.className = component.className;
|
|
17
17
|
if (component.properties) {
|
|
18
|
-
|
|
18
|
+
// Avoid overwriting the component dispatch keys (`type`/`id`) with inner
|
|
19
|
+
// renderer-specific props. E.g. PageTabsProps.properties.type is the tab
|
|
20
|
+
// visual style ('line'|'card'|'pill'), NOT the component type.
|
|
21
|
+
for (const [k, v] of Object.entries(component.properties)) {
|
|
22
|
+
if (k === 'type' || k === 'id')
|
|
23
|
+
continue;
|
|
24
|
+
node[k] = v;
|
|
25
|
+
}
|
|
26
|
+
// Preserve the original `properties` bag so renderers can still read
|
|
27
|
+
// collision-prone keys via `schema.properties.<key>`.
|
|
28
|
+
node.properties = component.properties;
|
|
19
29
|
}
|
|
20
30
|
if (component.events)
|
|
21
31
|
node.events = component.events;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/react",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "React bindings and SchemaRenderer component for Object UI",
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@objectstack/spec": "^4.0.5",
|
|
28
28
|
"react-hook-form": "^7.76.0",
|
|
29
|
-
"@object-ui/core": "4.
|
|
30
|
-
"@object-ui/data-objectstack": "4.
|
|
31
|
-
"@object-ui/i18n": "4.
|
|
32
|
-
"@object-ui/types": "4.
|
|
29
|
+
"@object-ui/core": "4.5.0",
|
|
30
|
+
"@object-ui/data-objectstack": "4.5.0",
|
|
31
|
+
"@object-ui/i18n": "4.5.0",
|
|
32
|
+
"@object-ui/types": "4.5.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": "^18.0.0 || ^19.0.0",
|