@object-ui/components 17.3.0 → 17.6.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 (47) hide show
  1. package/CHANGELOG.md +2732 -1
  2. package/README.md +38 -38
  3. package/dist/custom/button-group.d.ts +1 -1
  4. package/dist/custom/character-count.d.ts +56 -0
  5. package/dist/custom/config-field-renderer.d.ts +1 -1
  6. package/dist/custom/config-panel-renderer.d.ts +3 -3
  7. package/dist/custom/export-progress-dialog.d.ts +1 -1
  8. package/dist/custom/filter-builder.d.ts +284 -4
  9. package/dist/custom/fullscreen-editor.d.ts +102 -0
  10. package/dist/custom/index.d.ts +28 -27
  11. package/dist/custom/item.d.ts +1 -1
  12. package/dist/custom/resizable.d.ts +1 -1
  13. package/dist/debug/index.d.ts +2 -2
  14. package/dist/index.css +36 -6
  15. package/dist/index.d.ts +21 -20
  16. package/dist/index.js +4991 -4464
  17. package/dist/index.umd.cjs +10 -6
  18. package/dist/lib/form-control-dom-props.d.ts +77 -0
  19. package/dist/lib/sidebar-cookie.d.ts +55 -0
  20. package/dist/lib/slider-thumb.d.ts +86 -0
  21. package/dist/notifications/index.d.ts +5 -5
  22. package/dist/renderers/action/action-bar.d.ts +3 -3
  23. package/dist/renderers/action/action-button.d.ts +12 -2
  24. package/dist/renderers/action/action-group.d.ts +10 -4
  25. package/dist/renderers/action/action-icon.d.ts +10 -2
  26. package/dist/renderers/action/action-menu.d.ts +20 -2
  27. package/dist/renderers/action/auto-trigger.d.ts +20 -0
  28. package/dist/renderers/action/visibility-gate.d.ts +81 -0
  29. package/dist/renderers/basic/elements.d.ts +3 -3
  30. package/dist/renderers/basic/html-elements.d.ts +1 -1
  31. package/dist/renderers/basic/record-picker.d.ts +10 -1
  32. package/dist/renderers/complex/data-table.d.ts +100 -4
  33. package/dist/renderers/form/validationRuleKeys.d.ts +45 -0
  34. package/dist/renderers/layout/containers.d.ts +2 -1
  35. package/dist/renderers/layout/react-page.d.ts +12 -2
  36. package/dist/share/index.d.ts +2 -2
  37. package/dist/ui/calendar.d.ts +1 -1
  38. package/dist/ui/carousel.d.ts +2 -2
  39. package/dist/ui/command.d.ts +1 -1
  40. package/dist/ui/form.d.ts +1 -1
  41. package/dist/ui/index.d.ts +47 -47
  42. package/dist/ui/pagination.d.ts +1 -1
  43. package/dist/ui/sidebar.d.ts +2 -2
  44. package/dist/ui/slider.d.ts +2 -1
  45. package/package.json +14 -14
  46. package/dist/custom/action-param-dialog.d.ts +0 -21
  47. package/dist/renderers/data-display/table.d.ts +0 -2
@@ -0,0 +1,77 @@
1
+ import { DomProps as CoreDomProps } from '@object-ui/core';
2
+ /**
3
+ * The DOM pass-through declaration for the `packages/components` renderers whose
4
+ * host element is a FORM CONTROL (objectui#5632).
5
+ *
6
+ * ## Why this exists at all, rather than a bare `toDomProps`
7
+ *
8
+ * `toDomProps` is the SDUI contract's executor and its key list is deliberately
9
+ * ELEMENT-AGNOSTIC — it cannot know what element the widget renders, so it
10
+ * carries only what is legal on every element. `name` and `disabled` are legal
11
+ * on form controls and nowhere else, which is exactly why `dom-props.ts` leaves
12
+ * them out: `name` on the `<div>` a container widget renders is one of the 14
13
+ * attributes objectui#4431 closed.
14
+ *
15
+ * The renderers in this group render the other case. Routing them through the
16
+ * bare SDUI list would strip `name` and `disabled` off real controls — and
17
+ * `packages/fields` already wrote down what that costs, in the same words, for
18
+ * the same two keys:
19
+ *
20
+ * > Withholding them would make this helper a silent styling- and
21
+ * > interactivity-dropper … That is a NEW silent failure of exactly the kind
22
+ * > the whitelist exists to prevent, so they are forwarded.
23
+ *
24
+ * It is worth being precise about how INVISIBLE that failure would be here.
25
+ * The gate that grades this change
26
+ * (`packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx`) reports
27
+ * attributes that ARRIVE illegitimately; it has no case for an attribute that
28
+ * stops arriving, and its canary node authors no `disabled` at all. So dropping
29
+ * `disabled` off every form control in the library would have moved not one
30
+ * number in this card's own acceptance evidence. The ledger predicted the same
31
+ * thing from the other side — its `BARE_SPREAD_MINUS_NAME` note says converging
32
+ * this group "will not change that attribute, only the thirteen around it" —
33
+ * and that prediction is only true if `name` is forwarded deliberately.
34
+ *
35
+ * ## One mechanism, three declarations
36
+ *
37
+ * This is the third declaration over the ONE mechanism in
38
+ * `@object-ui/core`'s `pickDomProps`, and it follows the pattern
39
+ * `@object-ui/fields` established rather than inventing one: the consuming
40
+ * package owns the key list its own contract implies, and calls the shared
41
+ * filter with it. The three, and what separates them:
42
+ *
43
+ * - `toDomProps` (`@object-ui/core`) — the SDUI baseline, element-agnostic.
44
+ * - `@object-ui/fields`' `toDomProps` — the baseline minus `role`, plus `name`
45
+ * and `disabled`; `FieldWidgetComponentProps` declares the closed set.
46
+ * - this one — the baseline PLUS `name` and `disabled`, nothing removed. It is
47
+ * a strict superset of the SDUI list, which the compile-time assertion below
48
+ * pins, so a key added to the shared set can never be quietly dropped here.
49
+ *
50
+ * Anything beyond this set is still the objectui#4435 route: DECLARE it and
51
+ * forward it by name. Do not reopen the spread (AGENTS.md #0.1 — fix the
52
+ * contract, never widen the consumer). `style` is the standing example, and it
53
+ * is forwarded by name at every call site for exactly that reason.
54
+ */
55
+ declare const FORM_CONTROL_DOM_PASS_THROUGH_KEYS: readonly ["id", "className", "role", "tabIndex", "autoFocus", "onClick", "onBlur", "onFocus", "name", "disabled"];
56
+ type FormControlDomPassThroughKey = (typeof FORM_CONTROL_DOM_PASS_THROUGH_KEYS)[number];
57
+ /** The subset of `P` this declaration forwards, with each key's declared type. */
58
+ export type FormControlDomProps<P> = CoreDomProps<P, FormControlDomPassThroughKey>;
59
+ /**
60
+ * Keep only what may legitimately become an attribute on a FORM CONTROL, and
61
+ * drop everything else — the injected `schema`, the data-source adapter, the
62
+ * authored node's SDUI metadata, the flattened `props` container, and every
63
+ * declared prop the renderer already consumed off `schema`.
64
+ *
65
+ * Replaces the bare `{...rest}` spread in a form-control registration:
66
+ *
67
+ * ```tsx
68
+ * // before — forwards whatever `SchemaRenderer` handed it
69
+ * const { style, ...rest } = props;
70
+ * return <Input name={schema.name} {...rest} />;
71
+ *
72
+ * // after
73
+ * return <Input name={schema.name} {...toFormControlDomProps(rest)} style={style} />;
74
+ * ```
75
+ */
76
+ export declare function toFormControlDomProps<P extends object>(props: P): FormControlDomProps<P>;
77
+ export {};
@@ -0,0 +1,55 @@
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
+ * Reads the persisted sidebar collapse state back out of `document.cookie` —
10
+ * objectui#4234.
11
+ *
12
+ * `SidebarProvider` has always WRITTEN `sidebar_state` on every toggle (7-day
13
+ * max-age) and never read it back, so a collapsed sidebar came back expanded
14
+ * on the next load: the cookie was present and correct, and nothing consulted
15
+ * it. Upstream Shadcn closes that loop in a **server component** — it reads the
16
+ * cookie there and hands the value down as `defaultOpen`. A pure SPA (the
17
+ * console) has no such step, so the read has to happen client-side, which is
18
+ * what this function is for.
19
+ *
20
+ * ## Why the implementation lives HERE and not in the primitive
21
+ *
22
+ * `packages/components/src/ui/**` is regenerated from the Shadcn registry
23
+ * (AGENTS.md Commandment #7) — anything written there is overwritten by the
24
+ * next `pnpm shadcn:update`. `src/lib/` is not part of that regeneration, so
25
+ * this file is permanent. `sidebar.tsx` gets only a one-line call, re-applied
26
+ * on every sync by the declared patch in `scripts/shadcn-local-patches.mjs`.
27
+ * Keeping the payload out of the regenerated zone is what makes the patch small
28
+ * enough to survive upstream churn — the same shape as `close-label.tsx`.
29
+ *
30
+ * ## Why the cookie NAME is a parameter
31
+ *
32
+ * The name is owned by the write side: `SIDEBAR_COOKIE_NAME` in `sidebar.tsx`,
33
+ * the same constant the `document.cookie =` assignment interpolates. Passing it
34
+ * in keeps that the single source of truth — a second spelling here could drift
35
+ * from the writer and would read a cookie nobody writes. (Importing the
36
+ * constant instead would make `sidebar.tsx` and this module circular.)
37
+ *
38
+ * ## Strict values, deliberately
39
+ *
40
+ * Only the two literals the write side produces are accepted. Anything else —
41
+ * absent, empty, `"1"`, `"collapsed"`, a truncated value — returns `undefined`
42
+ * so the caller falls through to its `defaultOpen`. That is contract-first
43
+ * (AGENTS.md #0.1): the writer is ours and emits exactly `true`/`false`, so a
44
+ * tolerant parse here would only serve values no part of this system writes,
45
+ * and would silently invent a preference the user never expressed.
46
+ *
47
+ * @param name Cookie name to look up, matched EXACTLY (not by prefix or
48
+ * substring), so `sidebar_state_v2` / `my_sidebar_state` never answer for
49
+ * `sidebar_state`.
50
+ * @returns `true` / `false` for a persisted preference, or `undefined` when
51
+ * there is none to honour — including on the server, where there is no
52
+ * `document` to read (Next.js SSRs `"use client"` components for the initial
53
+ * HTML, so this guard is load-bearing for `apps/site`, not defensive noise).
54
+ */
55
+ export declare function readSidebarStateCookie(name: string): boolean | undefined;
@@ -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 type * as React from 'react';
9
+ /**
10
+ * The payload of the `slider-thumb-pass-through` local patch
11
+ * (`scripts/shadcn-local-patches.mjs`, objectui#3318).
12
+ *
13
+ * ## The defect this exists for
14
+ *
15
+ * A Radix slider's focusable control is the THUMB — `span[role="slider"]`,
16
+ * `tabindex="0"`, the element that carries `aria-valuenow` and the one a
17
+ * keyboard user and their screen reader actually land on. `Slider.Root` renders
18
+ * a role-less wrapper `span` and forwards every unrecognised prop to it, so a
19
+ * host's control-channel facts (`id`, `aria-invalid`, `aria-describedby`,
20
+ * `aria-labelledby`, `aria-required`) landed on a wrapper assistive technology
21
+ * never visits. Measured on `origin/main`, a real form + a required slider that
22
+ * had just failed validation:
23
+ *
24
+ * ```
25
+ * slider ariaInvalidTrue=[] labelFor=…-form-item -> DANGLING
26
+ * descConsumers=0 focusable=[span[slider],input]
27
+ * ids=[] <- no element of the row carried an id at all
28
+ * ```
29
+ *
30
+ * This is the same shape objectui#3306 fixed for `select`, one step harder:
31
+ * there the focusable control is `SelectTrigger`, a component the primitive
32
+ * EXPORTS, so the widget could address it directly. `ui/slider.tsx` renders its
33
+ * thumb internally and exports only `Slider`, so the widget has no handle on
34
+ * the one element that must carry the state — hence a pass-through.
35
+ *
36
+ * ## Why a declared prop and not an implicit bridge
37
+ *
38
+ * The primitive could instead have COPIED a fixed list of `aria-*` keys from
39
+ * its own props down to the thumb. Rejected: the copy would leave the same keys
40
+ * on Root as well, and for `id` that is not a duplicate attribute but a
41
+ * duplicate DOM ID — `document.getElementById` answers with the wrapper, so a
42
+ * host `<label for>` would resolve to the non-focusable span and the bug would
43
+ * survive its own fix wearing a green test. Splitting is therefore mandatory,
44
+ * and once the caller must say which keys go where, saying it explicitly is
45
+ * strictly better than a list the caller cannot see (AGENTS.md #0.1: one strict
46
+ * contract, no consumer-side guessing).
47
+ *
48
+ * ## Where the split is decided
49
+ *
50
+ * Not here — this only routes. The widget decides, and `SliderField` mirrors
51
+ * `SelectField`'s #3306 split exactly: `name` and `disabled` stay on Root (Root
52
+ * owns the hidden form input and is the single disabled authority), everything
53
+ * else in the DOM pass-through whitelist goes to the thumb.
54
+ */
55
+ /** Props the `Slider` wrapper hands to its internal `SliderPrimitive.Thumb`. */
56
+ export interface SliderThumbPassThrough {
57
+ /**
58
+ * Forwarded verbatim onto the focusable `span[role="slider"]`.
59
+ *
60
+ * Deliberately typed as the DOM attribute surface rather than
61
+ * `ComponentPropsWithoutRef<typeof SliderPrimitive.Thumb>`: this module must
62
+ * not import the primitive (it is consumed BY the no-touch file, and a cycle
63
+ * through it would be a fragile thing to hang a sync patch on), and the thumb
64
+ * is a plain `span` as far as every key a host delivers is concerned.
65
+ */
66
+ thumbProps?: React.HTMLAttributes<HTMLSpanElement> & {
67
+ id?: string;
68
+ tabIndex?: number;
69
+ 'aria-label'?: string;
70
+ };
71
+ }
72
+ /**
73
+ * Route a `Slider`'s props to the two elements that need them.
74
+ *
75
+ * `thumbProps` is REMOVED from the root half — leaving it there would put
76
+ * `thumbprops="[object Object]"` on the wrapper span, which is exactly the leak
77
+ * objectui#3291's sweep exists to catch.
78
+ *
79
+ * The pre-existing `aria-label` bridge is preserved and applied FIRST, so a
80
+ * caller that passes both keeps the explicit `thumbProps` value; nothing about
81
+ * the attribute's previous behaviour changes for callers that pass neither.
82
+ */
83
+ export declare function splitSliderThumbProps<P extends Record<string, unknown>>(props: P): {
84
+ root: Omit<P, 'thumbProps'>;
85
+ thumb: Record<string, unknown>;
86
+ };
@@ -19,8 +19,8 @@
19
19
  * Every one of these used to be handed to `onToast` regardless of type, so all
20
20
  * five looked identical (#3014).
21
21
  */
22
- export { NotificationBanners, type NotificationBannersProps } from './NotificationBanners';
23
- export { NotificationSnackbar, type NotificationSnackbarProps } from './NotificationSnackbar';
24
- export { NotificationAlerts, type NotificationAlertsProps } from './NotificationAlerts';
25
- export { NotificationInline, type NotificationInlineProps } from './NotificationInline';
26
- export { NOTIFICATION_ACTION_BUTTON_VARIANTS, NOTIFICATION_POSITION_CLASSES, NOTIFICATION_SEVERITY_STYLES, notificationActionVariant, notificationIcon, notificationSeverityStyle, type NotificationSeverityStyle, } from './severity';
22
+ export { NotificationBanners, type NotificationBannersProps } from './NotificationBanners.js';
23
+ export { NotificationSnackbar, type NotificationSnackbarProps } from './NotificationSnackbar.js';
24
+ export { NotificationAlerts, type NotificationAlertsProps } from './NotificationAlerts.js';
25
+ export { NotificationInline, type NotificationInlineProps } from './NotificationInline.js';
26
+ export { NOTIFICATION_ACTION_BUTTON_VARIANTS, NOTIFICATION_POSITION_CLASSES, NOTIFICATION_SEVERITY_STYLES, notificationActionVariant, notificationIcon, notificationSeverityStyle, type NotificationSeverityStyle, } from './severity.js';
@@ -1,8 +1,8 @@
1
- import { ActionSchema, ActionLocation } from '@object-ui/types';
1
+ import { UIActionSchema, ActionLocation } from '@object-ui/types';
2
2
  export interface ActionBarSchema {
3
3
  type: 'action:bar';
4
4
  /** Business actions to render — subject to inline/overflow split via {@link maxVisible} */
5
- actions?: ActionSchema[];
5
+ actions?: UIActionSchema[];
6
6
  /**
7
7
  * System/chrome actions (Duplicate, Export, View History, Delete, etc.) that
8
8
  * are *always* placed in the overflow menu — never inline — regardless of
@@ -13,7 +13,7 @@ export interface ActionBarSchema {
13
13
  * The first system action is automatically separated from business-overflow
14
14
  * entries by a menu separator.
15
15
  */
16
- systemActions?: ActionSchema[];
16
+ systemActions?: UIActionSchema[];
17
17
  /** Filter actions by this location */
18
18
  location?: ActionLocation;
19
19
  /** Maximum visible inline actions before overflow into "More" menu (default: 3) */
@@ -1,6 +1,16 @@
1
- import { ActionSchema } from '@object-ui/types';
1
+ import { UIActionSchema } from '@object-ui/types';
2
+ /**
3
+ * The declared props. `schema` is `UIActionSchema` (objectui#4418): every key
4
+ * this renderer forwards below — `target`, `endpoint`, `bodyExtra`,
5
+ * `bodyShape`, `locations`, `enabled`, `size` — is declared by the modern type
6
+ * and by no other, and the legacy `crud.ts` `ActionSchema` it used to name is
7
+ * `@deprecated` and requires `type: 'action'` (this renderer serves
8
+ * `'script' | 'url' | 'modal' | 'flow' | 'api'`). `actionType` stays on the
9
+ * intersection: it is the legacy-shaped override this renderer reads FIRST
10
+ * (`schema.actionType || schema.type`), and it is not a `UIActionSchema` key.
11
+ */
2
12
  export interface ActionButtonProps {
3
- schema: ActionSchema & {
13
+ schema: UIActionSchema & {
4
14
  type: string;
5
15
  className?: string;
6
16
  actionType?: string;
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- import { ActionSchema, ActionLocation } from '@object-ui/types';
2
+ import { UIActionSchema, ActionLocation } from '@object-ui/types';
3
3
  export interface ActionGroupSchema {
4
4
  type: 'action:group';
5
5
  /** Group name */
@@ -9,7 +9,7 @@ export interface ActionGroupSchema {
9
9
  /** Group icon */
10
10
  icon?: string;
11
11
  /** Actions in this group */
12
- actions?: ActionSchema[];
12
+ actions?: UIActionSchema[];
13
13
  /** Display mode: inline button row or dropdown */
14
14
  display?: 'dropdown' | 'inline';
15
15
  /** Filter actions by location */
@@ -35,7 +35,13 @@ export interface ActionGroupSchema {
35
35
  * showed even when its predicate was false.
36
36
  */
37
37
  export declare const DropdownActionItem: React.FC<{
38
- action: ActionSchema;
38
+ action: UIActionSchema;
39
39
  index: number;
40
- onSelect: (action: ActionSchema) => void | Promise<void>;
40
+ onSelect: (action: UIActionSchema) => void | Promise<void>;
41
+ /**
42
+ * The row this group is mounted over, forwarded by the host. Optional: an
43
+ * object-level group genuinely has no row, and a predicate over an empty
44
+ * record is still evaluable (objectui#4075).
45
+ */
46
+ record?: unknown;
41
47
  }>;
@@ -1,6 +1,14 @@
1
- import { ActionSchema } from '@object-ui/types';
1
+ import { UIActionSchema } from '@object-ui/types';
2
+ /**
3
+ * The declared props. `schema` is `UIActionSchema` (objectui#4418) for the same
4
+ * reason as `action:button`'s: `target`, `endpoint`, `bodyExtra`, `bodyShape`,
5
+ * `locations`, `enabled` and `size` are all modern-only keys this renderer
6
+ * forwards, and the legacy `crud.ts` `ActionSchema` is `@deprecated` and pins
7
+ * `type: 'action'` where this renderer's own registry `inputs` declare
8
+ * `'script' | 'url' | 'modal' | 'flow' | 'api'`.
9
+ */
2
10
  export interface ActionIconProps {
3
- schema: ActionSchema & {
11
+ schema: UIActionSchema & {
4
12
  type: string;
5
13
  className?: string;
6
14
  };
@@ -1,4 +1,5 @@
1
- import { ActionSchema } from '@object-ui/types';
1
+ import { default as React } from 'react';
2
+ import { UIActionSchema } from '@object-ui/types';
2
3
  export interface ActionMenuSchema {
3
4
  type: 'action:menu';
4
5
  /** Menu trigger label (defaults to icon-only) */
@@ -6,7 +7,7 @@ export interface ActionMenuSchema {
6
7
  /** Menu trigger icon (defaults to more-horizontal) */
7
8
  icon?: string;
8
9
  /** Actions to render as menu items */
9
- actions?: ActionSchema[];
10
+ actions?: UIActionSchema[];
10
11
  /** Trigger variant */
11
12
  variant?: string;
12
13
  /** Trigger size */
@@ -17,3 +18,20 @@ export interface ActionMenuSchema {
17
18
  className?: string;
18
19
  [key: string]: any;
19
20
  }
21
+ /**
22
+ * One action inside an `action:menu`. Exported for its pin tests only (it is
23
+ * not re-exported from the package index) — mirrors `DropdownActionItem` in
24
+ * `action-group.tsx`, whose gate is the same one.
25
+ */
26
+ export declare const ActionMenuItem: React.FC<{
27
+ action: UIActionSchema;
28
+ onExecute: (action: UIActionSchema) => Promise<void>;
29
+ /**
30
+ * The row this menu is mounted over, forwarded by the host. Optional: an
31
+ * object-level menu genuinely has no row, and a predicate over an empty
32
+ * record is still evaluable (objectui#4075 — the failure being fixed is a
33
+ * predicate that FAULTS on an unbound root, not one that reads a missing
34
+ * field).
35
+ */
36
+ record?: unknown;
37
+ }>;
@@ -0,0 +1,20 @@
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
+ * Is this action asking to be auto-triggered? One spelling of the test, so the
10
+ * flag cannot be read as `!== undefined` in one renderer and `=== true` in
11
+ * another. Deliberately strict: only the literal `true` arms it.
12
+ */
13
+ export declare function hasAutoTrigger(action: unknown): boolean;
14
+ /**
15
+ * Run `run` at most once, as soon as `armed` is true.
16
+ *
17
+ * `run` may change identity freely (it is rebuilt from `schema` on most
18
+ * renders); the ref is what makes this once-only, so depending on it is safe.
19
+ */
20
+ export declare function useAutoTriggerOnce(armed: boolean, run: () => void | Promise<void>): void;
@@ -0,0 +1,81 @@
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
+ * Did this action DECLARE a visibility gate at all? The renderer-side name for
10
+ * the repo's one definition of that question, which now lives one layer down in
11
+ * `@object-ui/core` as `hasDeclaredPredicate`
12
+ * (`packages/core/src/evaluator/declaredPredicate.ts`) — read the reasoning
13
+ * there; only what is specific to this file is repeated below.
14
+ *
15
+ * ## Why this module is now a re-export (objectui#3850 / objectui#3862)
16
+ *
17
+ * The question was answered three times with three different scopes, and the
18
+ * widest-scoped answer was on the key where the mistake is not benign:
19
+ *
20
+ * - HERE, `!= null && !== ''` — so every OBJECT counted as a declared gate,
21
+ * including `{ dialect: 'cel', source: '' }`. That envelope is not an exotic
22
+ * spelling: `@objectstack/spec`'s `ExpressionInputSchema` normalizes every
23
+ * authored predicate into one, so "the author left the predicate empty"
24
+ * compiles to exactly it. The verdict path then normalized the same value to
25
+ * `undefined` and `evaluateCondition(undefined)` answered `true` = "no
26
+ * condition, so visible/enabled" — which on `disabled` means GREYED OUT.
27
+ * A button disabled forever, indistinguishable from the metadata's intent
28
+ * (objectui#3850).
29
+ * - `SchemaRenderer`'s inline `!== undefined`, one notch wider again, so
30
+ * `disabled: null` greyed out any node on the generic rendering path
31
+ * (objectui#3862).
32
+ * - `ActionRunner`'s module-private helper, which asked "does this normalize
33
+ * to something evaluable?" — the scope objectui#3850 then ruled to be THE
34
+ * scope (objectui#3848).
35
+ *
36
+ * The ruling: a gate is declared when normalization still leaves a condition to
37
+ * evaluate. `''`, a whitespace-only string, an empty-`source` envelope and any
38
+ * non-predicate value are therefore NOT declared, and one definition in core
39
+ * says so for the renderers, `SchemaRenderer` and the execution entry alike.
40
+ *
41
+ * The NAME is kept (`hasDeclaredVisibilityGate`) and this module keeps
42
+ * re-exporting it: the five member-action call sites — `action:button`,
43
+ * `action:icon`, `action:group`'s inline button and dropdown item, and
44
+ * `action:menu`'s item, plus `DeclaredActionsBar` and `record-quick-actions`
45
+ * through the `@object-ui/components` barrel — are unchanged by this move. The
46
+ * name is historic (objectui#3492 arrived through `visible`) and the predicate is
47
+ * key-neutral, which the call-site comments already say.
48
+ *
49
+ * ## What did NOT change: truthiness still cannot answer this
50
+ *
51
+ * `if (action.visible && !isVisible)` was the objectui#3812 defect: it
52
+ * classified `visible: false` — the most explicit way an author can say "never
53
+ * show this" — as *ungated*, so the gate never consulted the verdict and the
54
+ * action rendered for everyone. A declared boolean is still a declared gate
55
+ * here; only the empty and non-predicate shapes moved.
56
+ *
57
+ * ## The scope change is not verdict-neutral on every key — measured
58
+ *
59
+ * On `visible` the empty shapes were mostly benign already, because the two
60
+ * mistakes cancelled: over-broad "declared" plus `evaluateCondition` answering
61
+ * `true` for an empty predicate = SHOWN, the same result as "no gate". Measured
62
+ * on this tree (`action:button`, before → after):
63
+ *
64
+ * | value | `visible` | `disabled` | `enabled` |
65
+ * |------------------------------|-------------------|-------------------|-------------------|
66
+ * | `''` | shown → shown | on → on | on → on |
67
+ * | `{ dialect:'cel', source:'' }`| shown → shown | GREY → on | on → on |
68
+ * | `{ source: '' }` | shown → shown | GREY → on | on → on |
69
+ * | `' '` (whitespace) | HIDDEN → shown | on → on | GREY → on |
70
+ * | `0` / `{}` (not a predicate) | shown → shown | GREY → on | on → on |
71
+ * | `true` / `false` / CEL / `${…}`| unchanged | unchanged | unchanged |
72
+ *
73
+ * The whitespace row is the one that moves on `visible`, and it moves toward the
74
+ * invariant rather than away from it: `' '` used to be normalized to `'${ }'`
75
+ * (the normalizer wraps it rather than folding it), which evaluates falsy, so a
76
+ * predicate that says nothing HID the action from everyone. "An empty predicate
77
+ * must not hide the action from everyone" is what this gate has always claimed —
78
+ * the whitespace spelling simply never obeyed it. Same story on the negated
79
+ * `enabled` leg, where it greyed the control out instead.
80
+ */
81
+ export { hasDeclaredPredicate as hasDeclaredVisibilityGate } from '@object-ui/core';
@@ -15,9 +15,9 @@
15
15
  * - element:divider (no-props separator)
16
16
  * - ElementButtonProps -> element:button
17
17
  *
18
- * Heavier interactive elements (element:filter, element:form) live in their
19
- * owning plugins and are left to those packages. element:record_picker — which
20
- * writes its selection into a page variable — ships alongside in `./record-picker`.
18
+ * Heavier interactive elements (element:form) live in their owning plugins and
19
+ * are left to those packages. element:record_picker — which writes its
20
+ * selection into a page variable — ships alongside in `./record-picker`.
21
21
  *
22
22
  * All props are read off `schema.properties` per the spec's
23
23
  * `UIComponent.properties` convention; `schema.props` is also accepted
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Safe native HTML element passthrough renderers (ADR: kind:'html').
9
9
  *
10
- * A `kind:'html'` page is authored as a constrained JSX/Tailwind string that is
10
+ * A `kind:'html'` page is authored as a constrained JSX string that is
11
11
  * PARSED (never executed) into the SDUI tree. For that tier to live up to its
12
12
  * name, the everyday HTML tags an author reaches for — headings, paragraphs,
13
13
  * lists, links, images, emphasis — must each resolve to a renderer (otherwise
@@ -9,7 +9,7 @@
9
9
  * record of an object and writes the selection into a page variable.
10
10
  *
11
11
  * Data binding follows the spec's ElementDataSource (`schema.dataSource`):
12
- * { object, filter?, sort?, limit? }
12
+ * { object, view?, filter?, sort?, limit? }
13
13
  * with `properties.object` accepted as a fallback. Display config is read off
14
14
  * `schema.properties`:
15
15
  * { labelField='name', valueField='id', label?, placeholder?, emptyText? }
@@ -20,6 +20,15 @@
20
20
  * picker is uncontrolled (still usable, just inert) so it never throws outside
21
21
  * a Page. The written value drives any predicate referencing `page.<var>`
22
22
  * (e.g. another component's `visible` / `visibility`).
23
+ *
24
+ * `view` is resolved through {@link useElementDataSource} rather than read off
25
+ * the binding directly (objectstack#6953). This block used to take `object` /
26
+ * `filter` / `sort` / `limit` off `schema.dataSource` and DROP `view`, so
27
+ * `dataSource: { object: 'account', view: 'hot' }` — the spec's own example —
28
+ * built an unfiltered picker over every account instead of the rows the saved
29
+ * view selects. That symptom is quieter than the one objectstack#5576 fixed on
30
+ * `list-view`: nothing errors, the list is simply WIDER than what was authored,
31
+ * which is exactly the failure an AI-authored page hides best.
23
32
  */
24
33
  import * as React from 'react';
25
34
  declare function ElementRecordPickerRenderer({ schema }: {
@@ -7,6 +7,101 @@ import { DataTableSchema } from '@object-ui/types';
7
7
  * only reachable structurally through `DataTableSchema`.
8
8
  */
9
9
  type RowActionDef = NonNullable<DataTableSchema['rowActionDefs']>[number];
10
+ /**
11
+ * The authoring shape of the built-in row Edit/Delete predicates — derived from
12
+ * `DataTableSchema`, never restated by hand (objectui#4354). Everything below
13
+ * reads what the production caller hands it (`schema.rowEditPredicates` /
14
+ * `rowDeletePredicates`), so a hand-written `{ visibleWhen?: unknown; … }` here
15
+ * would be a second definition of one authoring shape, free to drift from the
16
+ * type it is supposed to mirror without a single test going red.
17
+ *
18
+ * The union of the two twins is deliberate: every consumer below serves BOTH
19
+ * built-ins (`name: 'edit' | 'delete'`), so it may only read keys that BOTH
20
+ * schema keys declare — if either twin dropped one, the read stops compiling
21
+ * instead of silently reading `undefined`.
22
+ */
23
+ type BuiltinRowPredicates = NonNullable<DataTableSchema['rowEditPredicates'] | DataTableSchema['rowDeletePredicates']>;
24
+ /**
25
+ * Does the built-in Edit/Delete item render for THIS row? The single
26
+ * definition, read both by the item itself and by the row-level guard that
27
+ * decides whether the "⋮" trigger exists at all. The two disagreeing is
28
+ * objectui#3562: the guard counted handlers, the items were then filtered by
29
+ * these predicates, and a row with every item suppressed still got a trigger
30
+ * that opened an empty 128×10 box.
31
+ *
32
+ * `visibleWhen` counts as a declared gate by `!= null`, not by truthiness —
33
+ * `visibleWhen: false` hides the item rather than reading as "ungated".
34
+ *
35
+ * The parameter is consumption-shaped — visibility is all this decides, so
36
+ * `disabledWhen` is deliberately absent — but `Pick`ed from the authoring type
37
+ * rather than hand-written, so it is a SUBSET of that type by construction
38
+ * (objectui#4354).
39
+ */
40
+ export declare function isBuiltinRowActionVisible(predicates: Pick<BuiltinRowPredicates, 'visibleWhen'> | undefined, name: 'edit' | 'delete', row: any, scope: Record<string, unknown>): boolean;
41
+ /**
42
+ * Does this schema-driven custom row action render for THIS row? Same
43
+ * single-definition rule as the built-ins above.
44
+ *
45
+ * A gate counts as DECLARED by `!= null && !== ''`, never by truthiness
46
+ * (objectui#3758). Truthiness cannot answer the question: `visible: false` is a
47
+ * declared gate that excludes every row, and testing `!action.visible`
48
+ * classified it as *ungated* — so the most explicit way to say "never show this"
49
+ * rendered the item for everyone, and counted toward the "⋮" guard. This is the
50
+ * invariant objectui#3492 established for the selection bar (plugin-grid's
51
+ * `hasVisibilityGate`), and the same `!= null` posture the built-in `visibleWhen`
52
+ * gate above has always had. The boolean then decides in
53
+ * {@link evalRowActionVisibility}, which short-circuits it instead of handing it
54
+ * to the engine.
55
+ *
56
+ * `''` is grouped with `null` deliberately: an empty predicate is nothing to
57
+ * evaluate, so it must not hide the item from everyone either.
58
+ */
59
+ export declare function isCustomRowActionVisible(action: {
60
+ name?: string;
61
+ visible?: unknown;
62
+ } | undefined, row: any, scope: Record<string, unknown>): boolean;
63
+ /** What the row overflow menu will actually render for ONE row. */
64
+ export interface DataTableRowMenuPlan {
65
+ /** The built-in Edit item renders for this row. */
66
+ edit: boolean;
67
+ /** The built-in Delete item renders for this row. */
68
+ remove: boolean;
69
+ /** Custom actions surviving their own `visible` predicate, in declared order. */
70
+ custom: RowActionDef[];
71
+ /** Items that will render. `0` means: render no trigger at all (#3562). */
72
+ count: number;
73
+ }
74
+ /**
75
+ * Resolve the row overflow menu for ONE row — which items survive their
76
+ * per-record predicates, and how many that leaves.
77
+ *
78
+ * Per ROW, not per table: `visibleWhen` / `visible` are per-record predicates,
79
+ * so two rows of the same table legitimately differ (one keeps Edit, the next
80
+ * has nothing left). A table-level guard cannot express that, which is why the
81
+ * trigger decision lives here and is recomputed for every row.
82
+ *
83
+ * Only visibility is decided here — a `disabled` item still renders (greyed
84
+ * out) and still counts, exactly as before. That counting is pinned where it
85
+ * can be OBSERVED, on the rendered menu, by
86
+ * `data-table-row-menu-empty-guard.test.tsx`'s "keeps the trigger for a row
87
+ * whose only item renders merely DISABLED".
88
+ *
89
+ * The predicate parameters are consumption-shaped — this function reads
90
+ * `visibleWhen` and nothing else, and the signature keeps saying so — but each
91
+ * is `Pick`ed from the authoring key its production caller passes, rather than
92
+ * hand-restated (objectui#4354). Same subset, minus the drift: a rename in
93
+ * `@object-ui/types` fails here at compile time instead of leaving a stale
94
+ * hand-copy that still type-checks against nothing.
95
+ */
96
+ export declare function planDataTableRowMenu(input: {
97
+ onRowEdit?: unknown;
98
+ onRowDelete?: unknown;
99
+ editPredicates?: Pick<NonNullable<DataTableSchema['rowEditPredicates']>, 'visibleWhen'>;
100
+ deletePredicates?: Pick<NonNullable<DataTableSchema['rowDeletePredicates']>, 'visibleWhen'>;
101
+ customActions: readonly RowActionDef[];
102
+ row: any;
103
+ scope: Record<string, unknown>;
104
+ }): DataTableRowMenuPlan;
10
105
  /**
11
106
  * One schema-driven custom row action in the data-table's inline row overflow
12
107
  * menu. Extracted into its own component so the action's `visible` (and
@@ -50,10 +145,11 @@ export declare const DataTableRowActionItem: React.FC<{
50
145
  */
51
146
  export declare const DataTableBuiltinRowActionItem: React.FC<{
52
147
  name: 'edit' | 'delete';
53
- predicates?: {
54
- visibleWhen?: unknown;
55
- disabledWhen?: unknown;
56
- };
148
+ /**
149
+ * The whole authoring pair, because this component reads both keys — derived
150
+ * from `DataTableSchema` rather than hand-copied (objectui#4354).
151
+ */
152
+ predicates?: BuiltinRowPredicates;
57
153
  row: any;
58
154
  icon: React.ReactNode;
59
155
  label: string;