@object-ui/components 17.6.0 → 18.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 +9618 -0
- package/README.md +27 -3
- package/dist/custom/RecordTitleChip.d.ts +4 -3
- package/dist/custom/button-group.d.ts +1 -1
- package/dist/custom/combobox.d.ts +2 -4
- package/dist/custom/dialog-viewport-gutter.d.ts +37 -0
- package/dist/custom/empty-state-link.d.ts +63 -0
- package/dist/custom/empty.d.ts +16 -4
- package/dist/custom/field.d.ts +1 -1
- package/dist/custom/filter-builder.d.ts +225 -77
- package/dist/custom/grouping-editor.d.ts +4 -0
- package/dist/custom/horizontal-overflow.d.ts +18 -0
- package/dist/custom/index.d.ts +4 -1
- package/dist/custom/item.d.ts +1 -1
- package/dist/custom/kbd.d.ts +1 -1
- package/dist/custom/mobile-dialog-content.d.ts +22 -1
- package/dist/custom/navigation-overlay.d.ts +105 -0
- package/dist/custom/refresh-indicator.d.ts +8 -2
- package/dist/custom/sort-builder.d.ts +8 -0
- package/dist/custom/use-horizontal-overflow.d.ts +36 -0
- package/dist/custom/view-states.d.ts +74 -1
- package/dist/hooks/related-count-store.d.ts +81 -28
- package/dist/index.css +345 -361
- package/dist/index.d.ts +11 -3
- package/dist/index.js +8480 -6628
- package/dist/index.umd.cjs +11 -15
- package/dist/lib/close-label.d.ts +1 -2
- package/dist/lib/date-fns-locale.d.ts +41 -0
- package/dist/lib/inline-markdown.d.ts +64 -0
- package/dist/lib/lazy-icon.d.ts +32 -1
- package/dist/lib/lucide-record-icon-names.d.ts +30 -0
- package/dist/lib/utils.d.ts +113 -0
- package/dist/renderers/action/action-bar.d.ts +8 -36
- package/dist/renderers/action/action-button.d.ts +15 -4
- package/dist/renderers/action/action-group.d.ts +5 -4
- package/dist/renderers/action/action-icon.d.ts +12 -2
- package/dist/renderers/action/action-menu.d.ts +5 -2
- package/dist/renderers/action/auto-trigger.d.ts +19 -4
- package/dist/renderers/action/disabled-reason.d.ts +49 -0
- package/dist/renderers/action/resolve-icon.d.ts +27 -2
- package/dist/renderers/action/static-params.d.ts +71 -0
- package/dist/renderers/action/visibility-gate.d.ts +11 -3
- package/dist/renderers/basic/data-list.d.ts +2 -1
- package/dist/renderers/basic/elements.d.ts +2 -0
- package/dist/renderers/basic/html-elements.d.ts +7 -3
- package/dist/renderers/basic/readProps.d.ts +60 -0
- package/dist/renderers/basic/record-picker.d.ts +3 -2
- package/dist/renderers/complex/data-table.d.ts +31 -14
- package/dist/renderers/complex/dataTableBindDiagnostic.d.ts +187 -0
- package/dist/renderers/layout/box.d.ts +8 -0
- package/dist/renderers/layout/containers.d.ts +28 -23
- package/dist/renderers/layout/page-block-grid.d.ts +29 -0
- package/dist/renderers/layout/page-block-span.d.ts +50 -0
- package/dist/renderers/layout/page-description-context.d.ts +14 -0
- package/dist/renderers/layout/page-print.d.ts +14 -0
- package/dist/renderers/layout/page-region-posture.d.ts +82 -0
- package/dist/renderers/layout/react-page.d.ts +4 -0
- package/dist/renderers/navigation/sidebar.d.ts +31 -0
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/calendar.d.ts +2 -1
- package/dist/ui/command.d.ts +7 -7
- package/dist/ui/index.d.ts +0 -1
- package/dist/ui/input-otp.d.ts +2 -2
- package/dist/ui/sheet.d.ts +1 -1
- package/dist/ui/sidebar.d.ts +2 -1
- package/package.json +14 -15
- package/dist/SchemaRenderer.d.ts +0 -4
- package/dist/custom/export-progress-dialog.d.ts +0 -43
- package/dist/hooks/use-export-job.d.ts +0 -34
- package/dist/ui/chart.d.ts +0 -56
- package/dist/ui/toast.d.ts +0 -22
|
@@ -42,8 +42,35 @@ export interface NavigationOverlayProps {
|
|
|
42
42
|
mainContent?: React.ReactNode;
|
|
43
43
|
/**
|
|
44
44
|
* Popover trigger element (for popover mode).
|
|
45
|
+
*
|
|
46
|
+
* Rendered through Radix's `PopoverTrigger asChild`, so it is mounted HERE,
|
|
47
|
+
* inside the overlay's own subtree. That makes it the right carrier only for
|
|
48
|
+
* a host that has no element of its own to point at — a host wanting the
|
|
49
|
+
* popover to sit on an element it already rendered elsewhere (a grid row, a
|
|
50
|
+
* tree node, a gantt bar, a kanban card, a calendar event) must use
|
|
51
|
+
* {@link NavigationOverlayProps.popoverAnchorRef}, because re-rendering that
|
|
52
|
+
* element here would draw a SECOND copy of it in the overlay rather than
|
|
53
|
+
* anchor to the first.
|
|
45
54
|
*/
|
|
46
55
|
popoverTrigger?: React.ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* The already-rendered element the popover should be anchored to
|
|
58
|
+
* (popover mode).
|
|
59
|
+
*
|
|
60
|
+
* ⭐ This is how the five list-type renderers honour `navigation.mode:
|
|
61
|
+
* 'popover'` after objectui#9299: the click handler stores
|
|
62
|
+
* `event.currentTarget` — the row / node / bar / card / event the user
|
|
63
|
+
* actually clicked — on this ref in the same turn that opens the overlay,
|
|
64
|
+
* and the popover is positioned against it through Radix's virtual-anchor
|
|
65
|
+
* shape (`Popper.Anchor virtualRef`). No element is re-rendered, so nothing
|
|
66
|
+
* is duplicated and the renderer keeps ownership of its own DOM.
|
|
67
|
+
*
|
|
68
|
+
* When neither this nor `popoverTrigger` is supplied the overlay falls back
|
|
69
|
+
* to a compact `Dialog`. ⛔ After objectui#9299 none of the five renderers is
|
|
70
|
+
* allowed to reach that fallback — it survives only for a host that genuinely
|
|
71
|
+
* has no anchor to offer.
|
|
72
|
+
*/
|
|
73
|
+
popoverAnchorRef?: React.RefObject<HTMLElement | null>;
|
|
47
74
|
/**
|
|
48
75
|
* Optional handler invoked when the user clicks the "Expand to full page"
|
|
49
76
|
* affordance in the drawer/modal header. Mirrors Linear / Notion / Airtable
|
|
@@ -72,7 +99,85 @@ export interface NavigationOverlayProps {
|
|
|
72
99
|
* by the `width` prop / configured ceiling.
|
|
73
100
|
*/
|
|
74
101
|
storageKey?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Retired storage key whose value is migrated into `storageKey` once
|
|
104
|
+
* (drawer mode only).
|
|
105
|
+
*
|
|
106
|
+
* ⭐ objectui#9299 item 4: there used to be TWO drag-resize implementations
|
|
107
|
+
* with two different persisted keys and two different floors —
|
|
108
|
+
* `RecordDetailDrawer`'s `objectui.drawerWidth.OBJECT` and this component's
|
|
109
|
+
* `ov:STORAGE_KEY`. They are now one implementation, and a user who had
|
|
110
|
+
* already dragged a gantt / kanban / calendar drawer to their preferred width
|
|
111
|
+
* must keep that width rather than be silently reset to the default. So the
|
|
112
|
+
* first mount that finds no value under `storageKey` adopts the value found
|
|
113
|
+
* under this key, writes it forward, and REMOVES the retired entry — which is
|
|
114
|
+
* what makes the migration one-time rather than a permanent second read: a
|
|
115
|
+
* later double-click reset must not resurrect the old width.
|
|
116
|
+
*
|
|
117
|
+
* Build it with {@link legacyRecordDrawerWidthKey} rather than spelling the
|
|
118
|
+
* prefix again at a call site.
|
|
119
|
+
*/
|
|
120
|
+
legacyStorageKey?: string;
|
|
75
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The one `storageKey` spelling every record overlay uses, keyed by object.
|
|
124
|
+
*
|
|
125
|
+
* `NavigationOverlay` prefixes it with `ov:` on the way to `localStorage`, so
|
|
126
|
+
* the stored key is `ov:drawer-width:OBJECT`. The console's own object page
|
|
127
|
+
* (`app-shell`'s `ObjectView`) has always used this spelling; after
|
|
128
|
+
* objectui#9299 the five list-type renderers use it too, so one object has one
|
|
129
|
+
* remembered overlay width no matter which view type the user resized it on.
|
|
130
|
+
*/
|
|
131
|
+
export declare function recordOverlayWidthStorageKey(objectName: string): string;
|
|
132
|
+
/**
|
|
133
|
+
* The RETIRED per-object key `RecordDetailDrawer` persisted its width under
|
|
134
|
+
* before objectui#9299. Read once, migrated forward, then removed — see
|
|
135
|
+
* {@link NavigationOverlayProps.legacyStorageKey}. ⛔ Nothing writes it.
|
|
136
|
+
*/
|
|
137
|
+
export declare function legacyRecordDrawerWidthKey(objectName: string): string;
|
|
138
|
+
/**
|
|
139
|
+
* Remember the element a click landed on, so `popover` mode can anchor to it.
|
|
140
|
+
*
|
|
141
|
+
* ⭐ objectui#9299 item 3: `popover` is anchored to the clicked element — the
|
|
142
|
+
* row, node, bar, card or event the user actually pressed. That element is
|
|
143
|
+
* rendered by the RENDERER, not by this overlay, so the overlay cannot find it
|
|
144
|
+
* on its own, and `popoverTrigger` (which MOUNTS an element of its own inside
|
|
145
|
+
* the overlay) is the wrong carrier for it — passing the row there would draw
|
|
146
|
+
* a second copy of the row instead of pointing at the first.
|
|
147
|
+
*
|
|
148
|
+
* Two carriers, same recorder. Spread `anchorCaptureProps` on the renderer's
|
|
149
|
+
* own container when the click reaches it without a DOM event (a chart, a
|
|
150
|
+
* calendar grid); call `captureAnchor(event)` directly at a click site that
|
|
151
|
+
* already HAS one (a kanban card, a tree row). Either way hand `anchorRef` to
|
|
152
|
+
* {@link NavigationOverlayProps.popoverAnchorRef}. The container form uses the
|
|
153
|
+
* capture phase deliberately: it runs before the bubbling `onClick` that opens
|
|
154
|
+
* the overlay, so by the time the popover renders the anchor is recorded.
|
|
155
|
+
*
|
|
156
|
+
* It reads `target`, not `currentTarget`, because the handler sits on the
|
|
157
|
+
* CONTAINER: `currentTarget` there is the whole view, and a popover anchored
|
|
158
|
+
* to the whole board is not anchored to anything. `target` is the deepest
|
|
159
|
+
* element under the pointer — the bar, the card, the cell — which is what the
|
|
160
|
+
* user pointed at.
|
|
161
|
+
*
|
|
162
|
+
* ⚠️ Read synchronously, which is what this does: React nulls its event fields
|
|
163
|
+
* out once the handler returns, so stashing the event rather than the element
|
|
164
|
+
* would hand the overlay a dead reference.
|
|
165
|
+
*
|
|
166
|
+
* Clicks that do not open anything (a toolbar button, a filter) also land here
|
|
167
|
+
* and are harmless: `anchorRef` is only ever read while the popover is open,
|
|
168
|
+
* and the click that opened it is by construction the last one recorded.
|
|
169
|
+
*/
|
|
170
|
+
export declare function useOverlayAnchor(): {
|
|
171
|
+
anchorRef: React.RefObject<HTMLElement | null>;
|
|
172
|
+
captureAnchor: (event: {
|
|
173
|
+
target?: unknown;
|
|
174
|
+
} | null | undefined) => void;
|
|
175
|
+
anchorCaptureProps: {
|
|
176
|
+
onClickCapture: (event: {
|
|
177
|
+
target?: unknown;
|
|
178
|
+
}) => void;
|
|
179
|
+
};
|
|
180
|
+
};
|
|
76
181
|
/**
|
|
77
182
|
* NavigationOverlay — renders record detail in the configured overlay mode.
|
|
78
183
|
*
|
|
@@ -9,8 +9,14 @@ import * as React from "react";
|
|
|
9
9
|
export interface RefreshIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
10
10
|
/** Whether the indicator is active. When false, nothing is rendered. */
|
|
11
11
|
active: boolean;
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Accessible name of the progress bar, rendered as its `aria-label`.
|
|
14
|
+
* Required, with no default: the caller passes a string from its own
|
|
15
|
+
* translation layer, so the name a screen reader announces is in the active
|
|
16
|
+
* locale. An English default here reached every non-English screen-reader
|
|
17
|
+
* user through the callers that passed nothing (objectui#10580).
|
|
18
|
+
*/
|
|
19
|
+
ariaLabel: string;
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
16
22
|
* Thin, absolutely-positioned indeterminate progress bar that animates across
|
|
@@ -26,6 +26,14 @@ export interface SortBuilderProps {
|
|
|
26
26
|
fields?: Array<{
|
|
27
27
|
value: string;
|
|
28
28
|
label: string;
|
|
29
|
+
/**
|
|
30
|
+
* Listed but not choosable (objectui#11943). A disabled entry still names
|
|
31
|
+
* a row whose current field it is, so that row shows its label and can be
|
|
32
|
+
* changed or removed, but no row's dropdown lets the user pick it and
|
|
33
|
+
* "Add sort" never seeds it. "Add sort" seeds the first entry that is not
|
|
34
|
+
* disabled, and is itself disabled when every entry is.
|
|
35
|
+
*/
|
|
36
|
+
disabled?: boolean;
|
|
29
37
|
}>;
|
|
30
38
|
value?: SortItem[];
|
|
31
39
|
onChange?: (value: SortItem[]) => void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The horizontal-overflow affordance every wide table shares (objectui#12708):
|
|
4
|
+
* a box that scrolls sideways says so with a right-edge shadow while columns
|
|
5
|
+
* remain to its right, and shows its scrollbar while the pointer is over it or
|
|
6
|
+
* focus is inside it. Built once, here, and used by `data-table`'s own scroll
|
|
7
|
+
* box, `ObjectGrid`'s grouped scroller and any table a page mounts itself.
|
|
8
|
+
*/
|
|
9
|
+
/** Whether `box` can scroll further to the right (one pixel of slack for sub-pixel layouts). */
|
|
10
|
+
export declare function hasOverflowRight(box: Pick<HTMLElement, 'scrollLeft' | 'clientWidth' | 'scrollWidth'>): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Track {@link hasOverflowRight} for the box `ref` points at: read in a layout
|
|
13
|
+
* effect, so the first painted frame is already right, and again on every
|
|
14
|
+
* scroll and whenever the box or its content changes size. `false` while
|
|
15
|
+
* `disabled` (the box does not scroll on its own).
|
|
16
|
+
*/
|
|
17
|
+
export declare function useHorizontalOverflow(ref: React.RefObject<HTMLElement | null>, { disabled }?: {
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Scrollbar colours for a box that scrolls: transparent until the pointer is
|
|
22
|
+
* over the box or focus is inside it, then the theme's muted foreground. The bar
|
|
23
|
+
* keeps its place, so nothing shifts when it appears. Marked important
|
|
24
|
+
* because a host may theme every scrollbar with an unlayered `*` rule (the
|
|
25
|
+
* console does), which outranks any layered utility.
|
|
26
|
+
*/
|
|
27
|
+
export declare const HOVER_SCROLLBAR_CLASSES = "[scrollbar-width:thin]! [scrollbar-color:transparent_transparent]! hover:[scrollbar-color:var(--color-muted-foreground)_transparent]! focus-within:[scrollbar-color:var(--color-muted-foreground)_transparent]!";
|
|
28
|
+
/**
|
|
29
|
+
* Classes for a RIGHT-PINNED cell inside a {@link HorizontalOverflowBox} with
|
|
30
|
+
* `seam="pinned"`: the cell draws the shadow on its own left edge, the seam
|
|
31
|
+
* where the scrolling columns slide under it, while the box can scroll further
|
|
32
|
+
* right. Needs no measurement, because the pinned cell is where the seam is.
|
|
33
|
+
* Spelled out in full (as the edge overlay below is) so Tailwind's source scan
|
|
34
|
+
* sees every utility.
|
|
35
|
+
*/
|
|
36
|
+
export declare const PINNED_RIGHT_SEAM_SHADOW_CLASSES = "before:pointer-events-none before:absolute before:inset-y-0 before:right-full before:w-6 before:bg-gradient-to-l before:from-foreground/10 before:to-transparent before:opacity-0 group-data-[overflow-right=true]/hscroll:before:opacity-100";
|
|
@@ -44,8 +44,54 @@ interface DataEmptyStateProps extends React.ComponentProps<"div"> {
|
|
|
44
44
|
/** Optional action rendered below the description */
|
|
45
45
|
action?: React.ReactNode;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* `role` defaults to `"status"` (objectui#7132).
|
|
49
|
+
*
|
|
50
|
+
* The sibling states in this file each declare what they are — `DataLoadingState`
|
|
51
|
+
* is `role="status"`, `DataErrorState` is `role="alert"` — and the empty state
|
|
52
|
+
* alone declared nothing, so an empty box and a failed box were the same node
|
|
53
|
+
* shape to a screen reader and to any structural test. That is the exact
|
|
54
|
+
* property both the hotcrm#1212 (objectui#7063) and hotcrm#1247 (objectui#7064)
|
|
55
|
+
* rulings named first: an empty state must be *distinguishable from a load
|
|
56
|
+
* failure at a glance*.
|
|
57
|
+
*
|
|
58
|
+
* With no default, every surface that wanted the property had to type it at its
|
|
59
|
+
* own call site, and four independently did — `plugin-kanban`'s empty board,
|
|
60
|
+
* `plugin-dashboard`'s `WidgetEmptyState`, and `plugin-charts`' `ObjectChart` —
|
|
61
|
+
* while `plugin-list`, `plugin-detail`'s two timelines and the `ui:empty`
|
|
62
|
+
* renderer silently did not. Four hand-copies of one line is the per-app tax
|
|
63
|
+
* objectstack#13848 rules against, paid at the package level.
|
|
64
|
+
*
|
|
65
|
+
* It is a DEFAULT, not a fixed attribute: `role` is spread from `props` below,
|
|
66
|
+
* so a call site keeps the last word. That is what makes this change inert for
|
|
67
|
+
* the two ruled surfaces — both already pass `role="status"` explicitly and
|
|
68
|
+
* receive the identical attribute either way — and it is what lets a call site
|
|
69
|
+
* rendering something that is NOT empty say so.
|
|
70
|
+
*
|
|
71
|
+
* The call site that needed that last word was `plugin-list`'s load-failure
|
|
72
|
+
* panel, which borrowed this component for its layout. Since objectui#7143 it
|
|
73
|
+
* no longer does: `DataErrorState` grew the icon props it was missing and the
|
|
74
|
+
* panel is drawn by the component named for what it is. The default stays a
|
|
75
|
+
* default for the same reason it always was — a borrow can recur, and a fixed
|
|
76
|
+
* attribute would announce the next one as a routine status.
|
|
77
|
+
*/
|
|
47
78
|
declare function DataEmptyState({ className, icon, illustration, showIcon, iconWrapperClassName, title, description, action, children, ...props }: DataEmptyStateProps): React.JSX.Element;
|
|
48
79
|
interface DataErrorStateProps extends React.ComponentProps<"div"> {
|
|
80
|
+
/** Icon rendered above the title */
|
|
81
|
+
icon?: React.ReactNode;
|
|
82
|
+
/**
|
|
83
|
+
* When false, the icon container is omitted entirely. Useful for
|
|
84
|
+
* banner-style failures that should not draw a glyph at all. Defaults to
|
|
85
|
+
* true.
|
|
86
|
+
*/
|
|
87
|
+
showIcon?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Override class on the icon wrapper. By default the wrapper renders as a
|
|
90
|
+
* small destructive-tinted rounded square (`size-10 rounded-lg
|
|
91
|
+
* bg-destructive/10`). Pass `""` to strip that styling and render the icon
|
|
92
|
+
* raw, or extend the look (e.g. larger size).
|
|
93
|
+
*/
|
|
94
|
+
iconWrapperClassName?: string;
|
|
49
95
|
title?: string;
|
|
50
96
|
/** Error message or description */
|
|
51
97
|
message?: string;
|
|
@@ -54,5 +100,32 @@ interface DataErrorStateProps extends React.ComponentProps<"div"> {
|
|
|
54
100
|
/** Label for the retry button */
|
|
55
101
|
retryLabel?: string;
|
|
56
102
|
}
|
|
57
|
-
|
|
103
|
+
/**
|
|
104
|
+
* `icon` / `showIcon` / `iconWrapperClassName` MIRROR `DataEmptyState` above —
|
|
105
|
+
* same names, same types, same default semantics, including
|
|
106
|
+
* `iconWrapperClassName` REPLACING the wrapper's default class rather than
|
|
107
|
+
* merging with it, so `""` renders the icon raw (objectui#7143). A second
|
|
108
|
+
* spelling of the same three ideas is the defect the mirror exists to prevent;
|
|
109
|
+
* the only intended difference is the default class the `??` falls back to,
|
|
110
|
+
* which stays this component's own destructive square.
|
|
111
|
+
*
|
|
112
|
+
* They exist because `plugin-list` rendered its load FAILURE through
|
|
113
|
+
* `DataEmptyState` — the component named for the *empty* case — for want of
|
|
114
|
+
* them: this one hardcoded its glyph, so the one panel that needed to draw a
|
|
115
|
+
* network failure differently from a permission denial could only get that from
|
|
116
|
+
* the wrong component. objectui#7132 fixed the accessibility half of that
|
|
117
|
+
* collision (`role`) and deliberately left the structural half here.
|
|
118
|
+
*
|
|
119
|
+
* Deliberately NOT mirrored: `illustration` — an empty state's product-feel hero
|
|
120
|
+
* has no failure analogue — and `action`, because this component already spells
|
|
121
|
+
* its affordance as `onRetry` / `retryLabel`, with `children` for a call site
|
|
122
|
+
* that needs to render its own control.
|
|
123
|
+
*
|
|
124
|
+
* The icon wrapper carries `data-slot="data-error-state-icon"`, mirroring the
|
|
125
|
+
* empty state's `data-empty-state-icon`. Not a prop, and not in the ruling's
|
|
126
|
+
* list of three — it is here so the wrapper `iconWrapperClassName` now governs
|
|
127
|
+
* can be named by a test and a stylesheet, and so migrating a call site off
|
|
128
|
+
* `DataEmptyState` renames that identifier instead of dropping it.
|
|
129
|
+
*/
|
|
130
|
+
declare function DataErrorState({ className, icon, showIcon, iconWrapperClassName, title, message, onRetry, retryLabel, children, ...props }: DataErrorStateProps): React.JSX.Element;
|
|
58
131
|
export { DataLoadingState, DataEmptyState, DataEmptyState as EmptyState, DataErrorState, type DataLoadingStateProps, type DataEmptyStateProps, type DataEmptyStateProps as EmptyStateProps, type DataErrorStateProps, };
|
|
@@ -1,29 +1,14 @@
|
|
|
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
|
-
* Module-scoped store for related-list counts displayed in tab badges
|
|
9
|
-
* (e.g. "Contacts (12)" on an Account detail). The store deduplicates
|
|
10
|
-
* concurrent probes for the same key, lets the renderer subscribe with
|
|
11
|
-
* `useSyncExternalStore`, and exposes invalidation hooks so other parts
|
|
12
|
-
* of the runtime (bulk delete, inline create, optimistic update) can
|
|
13
|
-
* keep the badges in sync without a full page refetch.
|
|
14
|
-
*
|
|
15
|
-
* Design notes:
|
|
16
|
-
* - One global Map keyed by `${objectName}::${relField}::${parentId}` so
|
|
17
|
-
* a count fetched by one tab strip is reused by every other consumer.
|
|
18
|
-
* - Subscribers receive *all* keyspace changes; coarse-grained but
|
|
19
|
-
* badges are cheap to re-render and avoids per-key subscription noise.
|
|
20
|
-
* - We deliberately avoid Zustand here — the surface area is one Map +
|
|
21
|
-
* an emit() — and the React binding uses the built-in
|
|
22
|
-
* `useSyncExternalStore` so we don't grow the dependency graph.
|
|
23
|
-
*/
|
|
1
|
+
import { FieldContainerLike } from '@object-ui/core';
|
|
24
2
|
interface ProbeFn {
|
|
25
3
|
(objectName: string, query: {
|
|
26
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The parent scope alone (a MongoDB-style object, as it always was), or
|
|
6
|
+
* — once a list declares its own scope — the ObjectQL AST node
|
|
7
|
+
* `mergeFilterNodes` lowers the pair to. Both arms are what the ROW
|
|
8
|
+
* query on the same list already sends, so this union is not a new
|
|
9
|
+
* dialect: it is the same two shapes `RelatedList` puts on the wire.
|
|
10
|
+
*/
|
|
11
|
+
$filter?: Record<string, unknown> | unknown[];
|
|
27
12
|
$top?: number;
|
|
28
13
|
$count?: boolean;
|
|
29
14
|
}): Promise<{
|
|
@@ -33,13 +18,72 @@ interface ProbeFn {
|
|
|
33
18
|
length?: number;
|
|
34
19
|
}>;
|
|
35
20
|
}
|
|
36
|
-
|
|
37
|
-
|
|
21
|
+
/** What one read of a related list answered: the server total and its rows. */
|
|
22
|
+
interface ReadResult {
|
|
23
|
+
total: number;
|
|
24
|
+
rows: unknown[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A list's own declared scope, as carried on the `record:related_list` node it
|
|
28
|
+
* badges (objectui#4664 / objectstack#7118). Two probes over the SAME
|
|
29
|
+
* (object, relField, parent) triple are different questions when their scopes
|
|
30
|
+
* differ, so it is part of the cache identity below.
|
|
31
|
+
*/
|
|
32
|
+
export type CountScopeFilter = Record<string, any> | any[];
|
|
33
|
+
declare function getCount(objectName: string, relField: string | undefined, parentId: string | undefined, filter?: CountScopeFilter): number | undefined;
|
|
34
|
+
declare function setCount(objectName: string, relField: string | undefined, parentId: string | undefined, value: number, filter?: CountScopeFilter): void;
|
|
38
35
|
/**
|
|
39
36
|
* Probe a count via the supplied finder. Deduplicates concurrent requests
|
|
40
37
|
* for the same key and caches the resulting number until invalidated.
|
|
41
38
|
*/
|
|
42
|
-
declare function fetchCount(probe: ProbeFn, objectName: string, relField: string | undefined, parentId: string | undefined): Promise<number>;
|
|
39
|
+
declare function fetchCount(probe: ProbeFn, objectName: string, relField: string | undefined, parentId: string | undefined, filter?: CountScopeFilter, fields?: FieldContainerLike): Promise<number>;
|
|
40
|
+
/**
|
|
41
|
+
* objectui#13010 — the rail card's read: the first `limit` rows of the
|
|
42
|
+
* unfiltered related list and its total. Takes the rows a badge's read holds
|
|
43
|
+
* for this card's claim, joins a read in flight that asked for enough rows, or
|
|
44
|
+
* reads itself and registers that read in flight for a badge to join.
|
|
45
|
+
* Rejects when the read fails, so the card can show its error state.
|
|
46
|
+
*
|
|
47
|
+
* One exception to the registration: when a read with too FEW rows (a badge's
|
|
48
|
+
* one-row probe) is already in flight, this card's own read is NOT registered
|
|
49
|
+
* in flight. The too-small read stays the one a badge joins, and since only the
|
|
50
|
+
* current in-flight read installs a count, the card's own read installs none:
|
|
51
|
+
* the badge's read does.
|
|
52
|
+
*
|
|
53
|
+
* Contract: held rows are a HAND-OFF, not a cache. They are taken once (the
|
|
54
|
+
* first `fetchRows` that finds them removes them; a second call reads again),
|
|
55
|
+
* and they are dropped without being taken when the child object is
|
|
56
|
+
* invalidated (`invalidate` / `invalidateAll`, and the data-change bus that
|
|
57
|
+
* calls them), when the claim is released (`releaseRows`: the card unmounts or
|
|
58
|
+
* moves to a new scope), and when the read that fetched them was overtaken by
|
|
59
|
+
* either. The total is recorded in the store as `fetch` records it. A caller
|
|
60
|
+
* that must see data newer than the record open (a refresh) does not call this.
|
|
61
|
+
*/
|
|
62
|
+
declare function fetchRows(probe: ProbeFn, objectName: string, relField: string, parentId: string, limit: number, fields?: FieldContainerLike): Promise<ReadResult>;
|
|
63
|
+
/**
|
|
64
|
+
* objectui#13010 — a rail card says, while it renders, that it will list the
|
|
65
|
+
* first `limit` rows of this unfiltered related list. Idempotent, so a render
|
|
66
|
+
* React repeats or discards claims nothing new.
|
|
67
|
+
*
|
|
68
|
+
* Contract: a claim reads nothing by itself. While it stands, a `fetch` of the
|
|
69
|
+
* same unfiltered (object, relField, parentId) key asks for the claimed rows
|
|
70
|
+
* (the largest `limit` claimed) with its count and holds them for one
|
|
71
|
+
* `fetchRows` to take. A probe with a declared filter never meets a claim. The
|
|
72
|
+
* claim stands until `releaseRows` (unmount or new scope) or `_reset`; an
|
|
73
|
+
* invalidation drops held rows but leaves the claim.
|
|
74
|
+
*/
|
|
75
|
+
declare function claimRows(objectName: string, relField: string, parentId: string, limit: number): void;
|
|
76
|
+
/**
|
|
77
|
+
* objectui#13010 — the rail card is gone: its claim, the rows held for it and
|
|
78
|
+
* the read it started itself are dropped, so nothing outlives the record open.
|
|
79
|
+
*
|
|
80
|
+
* Contract: call it with the key a `claimRows` named, when the card unmounts
|
|
81
|
+
* or moves to a new scope (another parent or other entries). Afterwards a
|
|
82
|
+
* `fetch` of that key asks for one row again, untaken held rows are gone, and
|
|
83
|
+
* a later `fetchRows` does not join a read the card started itself. A read a
|
|
84
|
+
* badge started stays in flight for the badges that joined it.
|
|
85
|
+
*/
|
|
86
|
+
declare function releaseRows(objectName: string, relField: string, parentId: string): void;
|
|
43
87
|
/**
|
|
44
88
|
* Invalidate every cached count that involves the given object. Called by
|
|
45
89
|
* mutation paths (e.g. ObjectGrid's onBulkDelete callback, drawer save) so
|
|
@@ -55,7 +99,7 @@ declare function invalidateAll(): void;
|
|
|
55
99
|
* (object, relField, parentId) triple. Returns `undefined` while the
|
|
56
100
|
* probe is in flight or before the first request.
|
|
57
101
|
*/
|
|
58
|
-
export declare function useRelatedCount(objectName: string | undefined, relField: string | undefined, parentId: string | undefined): number | undefined;
|
|
102
|
+
export declare function useRelatedCount(objectName: string | undefined, relField: string | undefined, parentId: string | undefined, filter?: CountScopeFilter): number | undefined;
|
|
59
103
|
/**
|
|
60
104
|
* Subscribe to the store's monotonic version. Put it in an effect dependency
|
|
61
105
|
* array to RE-RUN count probes after an invalidation (#2269) — invalidate
|
|
@@ -67,11 +111,20 @@ export declare function useRelatedCountVersion(): number;
|
|
|
67
111
|
/**
|
|
68
112
|
* Imperative store API for non-React callers (mutation handlers, tests).
|
|
69
113
|
* Prefer `useRelatedCount` in components.
|
|
114
|
+
*
|
|
115
|
+
* `fetch`'s last parameter is the CHILD object's field defs. It is optional
|
|
116
|
+
* because a caller that cannot see metadata must still be able to probe — the
|
|
117
|
+
* seam then compiles the historical equality wire — but a caller that CAN see
|
|
118
|
+
* them owes them: that is the difference between a badge and no badge on a
|
|
119
|
+
* multi-value relationship (objectui#8882).
|
|
70
120
|
*/
|
|
71
121
|
export declare const RelatedCountStore: {
|
|
72
122
|
get: typeof getCount;
|
|
73
123
|
set: typeof setCount;
|
|
74
124
|
fetch: typeof fetchCount;
|
|
125
|
+
fetchRows: typeof fetchRows;
|
|
126
|
+
claimRows: typeof claimRows;
|
|
127
|
+
releaseRows: typeof releaseRows;
|
|
75
128
|
invalidate: typeof invalidate;
|
|
76
129
|
invalidateAll: typeof invalidateAll;
|
|
77
130
|
_reset: () => void;
|