@object-ui/react 4.7.0 → 5.0.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,123 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 927187a: Phase N.1 + N.2: visual polish for record detail pages.
8
+
9
+ **N.1 — System actions on full Lightning pages.** `PageHeaderRenderer`
10
+ now merges `headerSystemActions` from `RecordContext` with authored
11
+ actions (authored wins on name/id collision), so full custom pages
12
+ (lead, opportunity, ...) once again show 编辑 / 分享 / 删除 alongside
13
+ their authored actions. `sys_share` and `sys_delete` now use the
14
+ `outline` variant instead of `destructive` to read better in
15
+ multi-button clusters.
16
+
17
+ **N.2 — Hide empty fields by default in synth detail pages.**
18
+ `record:details` defaults `section.hideEmpty` to `true` so synthesized
19
+ pages don't render label graveyards on first load. The "显示 N 个空字段"
20
+ reveal toggle is preserved as the user-facing escape hatch. Authors can
21
+ opt back into showing every field by setting `hideEmpty: false` on the
22
+ section schema.
23
+
24
+ - 8435860: Phase N.4b: highlight↔body dedup now works for hand-authored Lightning
25
+ pages too.
26
+
27
+ Adds a small `HighlightFieldsContext` registry. `record:highlights`
28
+ registers the field names it currently surfaces; `record:details` unions
29
+ that live set into its `hideFieldNames` filter so a field shown in the
30
+ highlight strip is never duplicated in the section grid below.
31
+
32
+ Previously the dedup only fired for synth-generated pages (via the
33
+ `hideFields` prop passed by `buildDefaultPageSchema`). Custom Lightning
34
+ pages (e.g. opportunity) showed `所属客户` both in the strip and in the
35
+ body. The registry-based approach covers both code paths uniformly with
36
+ no schema author work required.
37
+
38
+ The registry uses `useSyncExternalStore` so adding/removing highlights
39
+ notifies consumers without triggering the provider value identity to
40
+ change — avoiding the update-loop that a naive context implementation
41
+ would cause.
42
+
43
+ `RecordDetailView` mounts `<HighlightFieldsProvider>` once per record
44
+ page so the two renderers share state.
45
+
46
+ - 74962b0: feat(detail): record:discussion schema component + flush accordion variant
47
+ - New `record:discussion` schema type lets authors place the record
48
+ chatter feed anywhere in a custom Page schema. Wired through a
49
+ shared `DiscussionContext` provider on the `assignedPage` branch
50
+ of `RecordDetailView`; auto-append still applies when no explicit
51
+ `record:discussion` / `record:chatter` node is present.
52
+ - `page:accordion` gains a `variant` prop. Default `flush` strips the
53
+ per-item border so accordion sections no longer double-wrap inner
54
+ Card-bearing renderers (RelatedList, etc.). Authors who want the
55
+ old visual pass `variant: 'card'`.
56
+ - `translateLabel` now handles compound labels split by `&`, `and`,
57
+ or `和` (e.g. `Notes & Attachments` → `备注与附件`).
58
+
59
+ - 7213027: feat(detail): slotted record pages (Track 3 Phase I)
60
+
61
+ Introduce `kind: "slotted"` record pages that override one or more
62
+ named slots while letting the default-page synthesizer fill in the
63
+ rest. Authors no longer need to re-author the entire page just to
64
+ customize the header or one tab.
65
+
66
+ **Slot menu (v1):**
67
+ - `header` — replaces `page:header`
68
+ - `actions` — replaces the `record:quick_actions` action bar
69
+ - `highlights` — replaces the chips + chevron path strip
70
+ - `details` — replaces the Details tab body (other tabs stay synthesized)
71
+ - `tabs` — replaces the entire `page:tabs` node (wins over `details`)
72
+ - `discussion` — replaces the inline `record:discussion` footer
73
+
74
+ Each slot is a full replacement at the slot boundary. To compose
75
+ default + custom, call the corresponding `buildDefault*` sub-builder
76
+ (now exported from `@object-ui/plugin-detail`):
77
+ `buildDefaultHeader`, `buildDefaultActions`, `buildDefaultHighlights`,
78
+ `buildDefaultDetails`, `buildDefaultTabs`, `buildDefaultDiscussion`.
79
+
80
+ **Author shape:**
81
+
82
+ ```ts
83
+ {
84
+ type: 'record',
85
+ object: 'account',
86
+ kind: 'slotted',
87
+ slots: {
88
+ header: { type: 'page:header', properties: { ... } },
89
+ },
90
+ }
91
+ ```
92
+
93
+ **API changes:**
94
+ - `PageSchema` (in `@object-ui/types`): adds `kind?: 'full' | 'slotted'`
95
+ (default `'full'`) and `slots?: PageSlotMap`.
96
+ - `usePageAssignment` (in `@object-ui/react`): result now exposes a
97
+ `slots` field populated when the matched page has `kind === 'slotted'`.
98
+ Existing `page` field is unchanged for full pages.
99
+ - `buildDefaultPageSchema` (in `@object-ui/plugin-detail`): accepts an
100
+ `options.slots` map that overrides individual regions at synthesis time.
101
+
102
+ ### Patch Changes
103
+
104
+ - Updated dependencies [8930b15]
105
+ - Updated dependencies [7213027]
106
+ - Updated dependencies [c7561a7]
107
+ - @object-ui/i18n@5.0.0
108
+ - @object-ui/types@5.0.0
109
+ - @object-ui/data-objectstack@5.0.0
110
+ - @object-ui/core@5.0.0
111
+
112
+ ## 4.8.0
113
+
114
+ ### Patch Changes
115
+
116
+ - @object-ui/types@4.8.0
117
+ - @object-ui/core@4.8.0
118
+ - @object-ui/i18n@4.8.0
119
+ - @object-ui/data-objectstack@4.8.0
120
+
3
121
  ## 4.7.0
4
122
 
5
123
  ### Patch Changes
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * DiscussionContext — shared chatter / discussion feed for the current
6
+ * record. Mounted alongside RecordContext by the host record page (see
7
+ * `RecordDetailView` in `@object-ui/app-shell`) so that any `record:chatter`
8
+ * or `record:discussion` renderer placed inside the schema tree picks up
9
+ * the same feed items / mutation handlers without a parallel fetch.
10
+ *
11
+ * Why this is separate from RecordContext: chatter feeds are commonly
12
+ * absent (e.g. settings pages, dashboards), and many record pages opt in
13
+ * via schema rather than always paying the fetch cost. Keeping the
14
+ * discussion data in its own context lets non-record pages provide it
15
+ * cheaply and lets record pages omit it without affecting unrelated
16
+ * record-aware renderers.
17
+ */
18
+ import React from 'react';
19
+ export interface DiscussionFeedItem {
20
+ id: string;
21
+ /** Generic discriminator — chatter renderers may switch on this. */
22
+ type?: string;
23
+ [k: string]: any;
24
+ }
25
+ export interface DiscussionContextValue {
26
+ items: DiscussionFeedItem[];
27
+ onAddComment?: (text: string, attachments?: any[]) => void | Promise<void>;
28
+ onAddReply?: (parentId: string, text: string) => void | Promise<void>;
29
+ onToggleReaction?: (itemId: string, reaction: string) => void | Promise<void>;
30
+ loading?: boolean;
31
+ error?: Error | null;
32
+ }
33
+ export interface DiscussionContextProviderProps extends DiscussionContextValue {
34
+ children: React.ReactNode;
35
+ }
36
+ export declare const DiscussionContextProvider: React.FC<DiscussionContextProviderProps>;
37
+ export declare function useDiscussionContext(): DiscussionContextValue | null;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * DiscussionContext — shared chatter / discussion feed for the current
7
+ * record. Mounted alongside RecordContext by the host record page (see
8
+ * `RecordDetailView` in `@object-ui/app-shell`) so that any `record:chatter`
9
+ * or `record:discussion` renderer placed inside the schema tree picks up
10
+ * the same feed items / mutation handlers without a parallel fetch.
11
+ *
12
+ * Why this is separate from RecordContext: chatter feeds are commonly
13
+ * absent (e.g. settings pages, dashboards), and many record pages opt in
14
+ * via schema rather than always paying the fetch cost. Keeping the
15
+ * discussion data in its own context lets non-record pages provide it
16
+ * cheaply and lets record pages omit it without affecting unrelated
17
+ * record-aware renderers.
18
+ */
19
+ import React from 'react';
20
+ const DiscussionContext = React.createContext(null);
21
+ export const DiscussionContextProvider = ({ children, ...value }) => {
22
+ const memo = React.useMemo(() => value, [
23
+ value.items,
24
+ value.onAddComment,
25
+ value.onAddReply,
26
+ value.onToggleReaction,
27
+ value.loading,
28
+ value.error,
29
+ ]);
30
+ return (_jsx(DiscussionContext.Provider, { value: memo, children: children }));
31
+ };
32
+ export function useDiscussionContext() {
33
+ return React.useContext(DiscussionContext);
34
+ }
@@ -13,6 +13,17 @@
13
13
  * receive props from the schema tree.
14
14
  */
15
15
  import React from 'react';
16
+ export declare const HighlightFieldsProvider: React.FC<{
17
+ children: React.ReactNode;
18
+ }>;
19
+ /** Subscribe to the live highlight-field set. Empty when no provider. */
20
+ export declare function useHighlightFieldNames(): ReadonlySet<string>;
21
+ /**
22
+ * Register a list of field names as currently surfaced by a
23
+ * `record:highlights` instance. Re-registers when the joined name list
24
+ * changes; unregisters on unmount.
25
+ */
26
+ export declare function useRegisterHighlightFields(instanceId: string, names: string[]): void;
16
27
  export interface RecordContextValue<TData = any, TObjectSchema = any> {
17
28
  /** Object machine name, e.g. "crm_opportunity". */
18
29
  objectName: string;
@@ -30,6 +41,16 @@ export interface RecordContextValue<TData = any, TObjectSchema = any> {
30
41
  error?: Error | null;
31
42
  /** Re-fetch the record from the source. */
32
43
  refresh?: () => void | Promise<void>;
44
+ /**
45
+ * Optional system-level header actions (Edit / Share / Delete) that the
46
+ * host (e.g. RecordDetailView) wants to inject into the page header
47
+ * regardless of whether the page schema is synthesised or authored.
48
+ *
49
+ * `PageHeaderRenderer` appends these AFTER any authored
50
+ * `page:header.actions`, deduplicating by `name`. Hosts pass `undefined`
51
+ * to opt out entirely (e.g. embedded previews).
52
+ */
53
+ headerSystemActions?: any[];
33
54
  /**
34
55
  * When true, the record is rendered inside a host overlay (drawer,
35
56
  * modal, split-pane preview) rather than as a standalone route. Header
@@ -14,6 +14,78 @@ import { jsx as _jsx } from "react/jsx-runtime";
14
14
  * receive props from the schema tree.
15
15
  */
16
16
  import React from 'react';
17
+ const HighlightFieldsContext = React.createContext(null);
18
+ export const HighlightFieldsProvider = ({ children }) => {
19
+ const registry = React.useMemo(() => {
20
+ const map = new Map();
21
+ let names = EMPTY_SET;
22
+ const listeners = new Set();
23
+ const recompute = () => {
24
+ const next = new Set();
25
+ for (const list of map.values())
26
+ for (const n of list)
27
+ next.add(n);
28
+ // Skip notify if shallow-equal to previous (avoid spurious updates).
29
+ if (next.size === names.size) {
30
+ let same = true;
31
+ for (const n of next)
32
+ if (!names.has(n)) {
33
+ same = false;
34
+ break;
35
+ }
36
+ if (same)
37
+ return;
38
+ }
39
+ names = next;
40
+ listeners.forEach((l) => l());
41
+ };
42
+ return {
43
+ getNames: () => names,
44
+ subscribe: (l) => {
45
+ listeners.add(l);
46
+ return () => { listeners.delete(l); };
47
+ },
48
+ register: (id, list) => {
49
+ const prev = map.get(id);
50
+ if (prev &&
51
+ prev.length === list.length &&
52
+ prev.every((n, i) => n === list[i])) {
53
+ return;
54
+ }
55
+ map.set(id, [...list]);
56
+ recompute();
57
+ },
58
+ unregister: (id) => {
59
+ if (map.delete(id))
60
+ recompute();
61
+ },
62
+ };
63
+ }, []);
64
+ return (_jsx(HighlightFieldsContext.Provider, { value: registry, children: children }));
65
+ };
66
+ /** Subscribe to the live highlight-field set. Empty when no provider. */
67
+ export function useHighlightFieldNames() {
68
+ const ctx = React.useContext(HighlightFieldsContext);
69
+ return React.useSyncExternalStore(ctx ? ctx.subscribe : NOOP_SUBSCRIBE, ctx ? ctx.getNames : GET_EMPTY, ctx ? ctx.getNames : GET_EMPTY);
70
+ }
71
+ /**
72
+ * Register a list of field names as currently surfaced by a
73
+ * `record:highlights` instance. Re-registers when the joined name list
74
+ * changes; unregisters on unmount.
75
+ */
76
+ export function useRegisterHighlightFields(instanceId, names) {
77
+ const ctx = React.useContext(HighlightFieldsContext);
78
+ const key = names.join('|');
79
+ React.useEffect(() => {
80
+ if (!ctx)
81
+ return;
82
+ ctx.register(instanceId, key.length === 0 ? [] : key.split('|'));
83
+ return () => ctx.unregister(instanceId);
84
+ }, [ctx, instanceId, key]);
85
+ }
86
+ const NOOP_SUBSCRIBE = (_) => () => { };
87
+ const GET_EMPTY = () => EMPTY_SET;
88
+ const EMPTY_SET = new Set();
17
89
  const RecordContext = React.createContext(null);
18
90
  export const RecordContextProvider = ({ children, ...value }) => {
19
91
  // Memoize so consumers that rely on referential equality don't re-render
@@ -28,6 +100,7 @@ export const RecordContextProvider = ({ children, ...value }) => {
28
100
  value.error,
29
101
  value.refresh,
30
102
  value.embedded,
103
+ value.headerSystemActions,
31
104
  ]);
32
105
  return _jsx(RecordContext.Provider, { value: memo, children: children });
33
106
  };
@@ -5,3 +5,4 @@ export * from './NotificationContext';
5
5
  export * from './DndContext';
6
6
  export * from './AppShellContext';
7
7
  export * from './RecordContext';
8
+ export * from './DiscussionContext';
@@ -5,3 +5,4 @@ export * from './NotificationContext';
5
5
  export * from './DndContext';
6
6
  export * from './AppShellContext';
7
7
  export * from './RecordContext';
8
+ export * from './DiscussionContext';
@@ -10,6 +10,14 @@
10
10
  * and returns the first PageSchema whose `pageType === 'record'` and `object`
11
11
  * matches the requested name.
12
12
  *
13
+ * Returns a discriminated result by `PageSchema.kind`:
14
+ * - `kind === 'full'` (default): the schema fully describes the page;
15
+ * the result populates `page` and the caller renders it as-is.
16
+ * - `kind === 'slotted'`: the schema only provides slot overrides;
17
+ * the result populates `slots` and the caller feeds them to
18
+ * `buildDefaultPageSchema(objectDef, { slots })` so omitted slots
19
+ * fall through to synthesized defaults.
20
+ *
13
21
  * Future work (deferred): recordType / profile / app / formFactor filtering
14
22
  * and priority-based selection. For now we return the first match so that
15
23
  * callers can deterministically fall back to the auto-generated DetailView
@@ -28,8 +36,24 @@ export interface PageAssignmentOptions {
28
36
  pageName?: string;
29
37
  }
30
38
  export interface PageAssignmentResult {
31
- /** Resolved PageSchema, or null if no record Page is available. */
39
+ /**
40
+ * Resolved full PageSchema, or null when none is available.
41
+ *
42
+ * Populated only when the matched page has `kind === 'full'` (the
43
+ * default — see `PageSchema.kind`). Slotted pages do NOT populate
44
+ * this field; callers should branch on `slots` instead.
45
+ */
32
46
  page: any | null;
47
+ /**
48
+ * Slot override map for the matched slotted page, or null when no
49
+ * slotted page matched.
50
+ *
51
+ * Populated only when the matched page has `kind === 'slotted'`.
52
+ * The caller is expected to feed these slots to the default-page
53
+ * synthesizer (`buildDefaultPageSchema(objectDef, { slots })`) so
54
+ * that omitted slots fall through to synthesized defaults.
55
+ */
56
+ slots: any | null;
33
57
  /** True while the metadata cache is still loading the `page` type. */
34
58
  loading: boolean;
35
59
  /** Loader error, if any. */
@@ -10,6 +10,14 @@
10
10
  * and returns the first PageSchema whose `pageType === 'record'` and `object`
11
11
  * matches the requested name.
12
12
  *
13
+ * Returns a discriminated result by `PageSchema.kind`:
14
+ * - `kind === 'full'` (default): the schema fully describes the page;
15
+ * the result populates `page` and the caller renders it as-is.
16
+ * - `kind === 'slotted'`: the schema only provides slot overrides;
17
+ * the result populates `slots` and the caller feeds them to
18
+ * `buildDefaultPageSchema(objectDef, { slots })` so omitted slots
19
+ * fall through to synthesized defaults.
20
+ *
13
21
  * Future work (deferred): recordType / profile / app / formFactor filtering
14
22
  * and priority-based selection. For now we return the first match so that
15
23
  * callers can deterministically fall back to the auto-generated DetailView
@@ -54,7 +62,7 @@ export function usePageAssignment(objectName, opts = {}) {
54
62
  cancelled = true;
55
63
  };
56
64
  }, [meta]);
57
- const page = useMemo(() => {
65
+ const matched = useMemo(() => {
58
66
  if (!objectName && !opts.pageName)
59
67
  return null;
60
68
  const pages = Array.isArray(meta.pages) ? meta.pages : [];
@@ -87,8 +95,20 @@ export function usePageAssignment(objectName, opts = {}) {
87
95
  candidates.sort((a, b) => (b?.priority ?? 0) - (a?.priority ?? 0));
88
96
  return candidates[0];
89
97
  }, [meta.pages, objectName, opts.pageName, opts.recordType, opts.profile, opts.app, opts.formFactor]);
98
+ // Discriminate by `kind`: full pages populate `page`, slotted pages
99
+ // populate `slots`. Missing `kind` defaults to 'full' for backwards
100
+ // compatibility with pre-Phase-I metadata.
101
+ const { page, slots } = useMemo(() => {
102
+ if (!matched)
103
+ return { page: null, slots: null };
104
+ if (matched.kind === 'slotted') {
105
+ return { page: null, slots: matched.slots ?? {} };
106
+ }
107
+ return { page: matched, slots: null };
108
+ }, [matched]);
90
109
  return {
91
110
  page,
111
+ slots,
92
112
  loading: meta.loading || !ensured,
93
113
  error: ensureError ?? meta.error ?? null,
94
114
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/react",
3
- "version": "4.7.0",
3
+ "version": "5.0.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.7.0",
30
- "@object-ui/data-objectstack": "4.7.0",
31
- "@object-ui/i18n": "4.7.0",
32
- "@object-ui/types": "4.7.0"
29
+ "@object-ui/core": "5.0.0",
30
+ "@object-ui/data-objectstack": "5.0.0",
31
+ "@object-ui/i18n": "5.0.0",
32
+ "@object-ui/types": "5.0.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^18.0.0 || ^19.0.0",