@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 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
@@ -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
- Object.assign(newSchema, newSchema.properties);
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
+ }
@@ -4,3 +4,4 @@ export * from './ThemeContext';
4
4
  export * from './NotificationContext';
5
5
  export * from './DndContext';
6
6
  export * from './AppShellContext';
7
+ export * from './RecordContext';
@@ -4,3 +4,4 @@ export * from './ThemeContext';
4
4
  export * from './NotificationContext';
5
5
  export * from './DndContext';
6
6
  export * from './AppShellContext';
7
+ export * from './RecordContext';
@@ -33,3 +33,4 @@ export * from './useGlobalUndo';
33
33
  export * from './useDebugMode';
34
34
  export * from './useActionEngine';
35
35
  export * from './useDataRefresh';
36
+ export * from './usePageAssignment';
@@ -33,3 +33,4 @@ export * from './useGlobalUndo';
33
33
  export * from './useDebugMode';
34
34
  export * from './useActionEngine';
35
35
  export * from './useDataRefresh';
36
+ export * from './usePageAssignment';
@@ -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
- Object.assign(node, component.properties);
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.0",
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.3.0",
30
- "@object-ui/data-objectstack": "4.3.0",
31
- "@object-ui/i18n": "4.3.0",
32
- "@object-ui/types": "4.3.0"
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",