@xh/hoist 86.1.0 → 86.3.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/.gitattributes +4 -0
- package/CHANGELOG.md +104 -0
- package/admin/AdminJsonDisplay.ts +31 -0
- package/admin/App.scss +3 -11
- package/admin/AppModel.ts +3 -4
- package/admin/jsonsearch/impl/JsonSearchImplModel.ts +4 -4
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +56 -6
- package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +9 -26
- package/admin/tabs/clients/ClientsModel.ts +4 -1
- package/admin/tabs/clients/activity/ClientDetailModel.ts +5 -1
- package/admin/tabs/cluster/instances/connpool/ConnPoolMonitorPanel.ts +3 -11
- package/admin/tabs/cluster/instances/services/DetailsPanel.ts +2 -13
- package/admin/tabs/cluster/objects/DetailPanel.ts +3 -12
- package/admin/tabs/userData/roles/RolePanel.ts +2 -2
- package/appcontainer/RouterModel.ts +6 -6
- package/build/types/admin/AdminJsonDisplay.d.ts +7 -0
- package/build/types/admin/AppModel.d.ts +3 -4
- package/build/types/admin/jsonsearch/impl/JsonSearchImplModel.d.ts +2 -2
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +14 -3
- package/build/types/admin/tabs/clients/ClientsModel.d.ts +3 -0
- package/build/types/appcontainer/RouterModel.d.ts +5 -5
- package/build/types/cmp/grid/Grid.d.ts +8 -4
- package/build/types/cmp/grid/GridModel.d.ts +13 -2
- package/build/types/cmp/grid/Types.d.ts +8 -1
- package/build/types/cmp/grid/filter/GridFilterFieldSpec.d.ts +24 -6
- package/build/types/cmp/input/SegmentedControlOption.d.ts +12 -0
- package/build/types/core/HoistAppModel.d.ts +2 -3
- package/build/types/core/types/Types.d.ts +12 -0
- package/build/types/desktop/cmp/button/grid/ExpandToLevelButton.d.ts +2 -1
- package/build/types/desktop/cmp/filter/FilterChooser.d.ts +5 -0
- package/build/types/desktop/cmp/filter/PopoverFilterChooser.d.ts +3 -1
- package/build/types/desktop/cmp/grid/impl/colchooser/ColChooserModel.d.ts +2 -2
- package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.d.ts +9 -1
- package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.d.ts +8 -2
- package/build/types/desktop/cmp/input/CodeInput.d.ts +7 -0
- package/build/types/desktop/cmp/input/NumberInput.d.ts +1 -1
- package/build/types/desktop/cmp/input/Select.d.ts +8 -0
- package/build/types/desktop/cmp/input/impl/CalcWindowedMenuWidth.d.ts +20 -0
- package/build/types/desktop/cmp/rest/impl/RestFormModel.d.ts +1 -1
- package/build/types/format/FormatNumber.d.ts +11 -3
- package/build/types/icon/Icon.d.ts +4 -3
- package/build/types/kit/react-dropzone/index.d.ts +1 -0
- package/build/types/kit/swiper/index.d.ts +4 -3
- package/build/types/mobile/cmp/button/grid/ExpandToLevelButton.d.ts +2 -1
- package/build/types/mobile/cmp/grid/impl/ColChooserModel.d.ts +2 -2
- package/build/types/mobile/cmp/input/NumberInput.d.ts +1 -1
- package/build/types/mobile/cmp/input/Select.d.ts +8 -0
- package/cmp/grid/Grid.ts +27 -2
- package/cmp/grid/GridModel.ts +21 -8
- package/cmp/grid/Types.ts +9 -1
- package/cmp/grid/filter/GridFilterFieldSpec.ts +28 -5
- package/cmp/grid/impl/MenuSupport.ts +3 -5
- package/cmp/input/SegmentedControlOption.ts +14 -0
- package/cmp/store/impl/StoreFilterFieldImplModel.ts +5 -6
- package/core/HoistAppModel.ts +2 -3
- package/core/types/Types.ts +14 -5
- package/desktop/cmp/button/grid/ExpandToLevelButton.ts +4 -5
- package/desktop/cmp/filechooser/FileChooser.ts +6 -3
- package/desktop/cmp/filechooser/FileChooserModel.ts +12 -5
- package/desktop/cmp/filter/FilterChooser.scss +41 -0
- package/desktop/cmp/filter/FilterChooser.ts +147 -7
- package/desktop/cmp/filter/PopoverFilterChooser.ts +7 -84
- package/desktop/cmp/grid/editors/NumberEditor.ts +16 -17
- package/desktop/cmp/grid/editors/SelectEditor.ts +15 -10
- package/desktop/cmp/grid/editors/impl/InlineEditorModel.ts +6 -1
- package/desktop/cmp/grid/find/impl/GridFindFieldImplModel.ts +3 -5
- package/desktop/cmp/grid/impl/colchooser/ColChooserModel.ts +2 -2
- package/desktop/cmp/grid/impl/filter/GridFilterDialog.ts +2 -2
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRow.ts +12 -12
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.ts +18 -0
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.scss +5 -0
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.ts +13 -2
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.ts +97 -14
- package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.ts +46 -7
- package/desktop/cmp/input/CheckboxButton.ts +21 -1
- package/desktop/cmp/input/CodeInput.ts +20 -3
- package/desktop/cmp/input/NumberInput.ts +1 -1
- package/desktop/cmp/input/SegmentedControl.ts +7 -3
- package/desktop/cmp/input/Select.ts +37 -18
- package/desktop/cmp/input/impl/CalcWindowedMenuWidth.ts +106 -0
- package/desktop/cmp/rest/impl/RestFormField.ts +1 -1
- package/desktop/cmp/tab/Tabs.scss +72 -25
- package/docs/routing.md +4 -2
- package/docs/upgrade-notes/v86-upgrade-notes.md +73 -8
- package/format/FormatNumber.ts +69 -32
- package/icon/Icon.scss +13 -0
- package/icon/Icon.ts +4 -3
- package/icon/impl/IconHtml.ts +1 -1
- package/kit/react-dropzone/index.ts +1 -0
- package/mcp/README.md +12 -0
- package/mcp/data/ts-registry.spec.ts +156 -0
- package/mcp/data/ts-registry.ts +14 -7
- package/mcp/formatters/typescript.ts +7 -3
- package/mcp/util/paths.ts +21 -0
- package/mobile/cmp/button/grid/ExpandToLevelButton.ts +4 -5
- package/mobile/cmp/grid/impl/ColChooserModel.ts +2 -2
- package/mobile/cmp/input/CheckboxButton.ts +18 -1
- package/mobile/cmp/input/NumberInput.ts +1 -1
- package/mobile/cmp/input/SegmentedControl.ts +7 -3
- package/mobile/cmp/input/Select.ts +21 -15
- package/mobx/README.md +2 -1
- package/package.json +10 -10
- package/styles/vars.scss +15 -0
- package/desktop/cmp/filter/PopoverFilterChooser.scss +0 -42
|
@@ -6,14 +6,23 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {FilterChooserFilter, FilterChooserModel} from '@xh/hoist/cmp/filter';
|
|
8
8
|
import {box, div, hbox, hframe, vbox} from '@xh/hoist/cmp/layout';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
hoistCmp,
|
|
11
|
+
HoistModel,
|
|
12
|
+
HoistProps,
|
|
13
|
+
LayoutProps,
|
|
14
|
+
lookup,
|
|
15
|
+
useLocalModel,
|
|
16
|
+
uses
|
|
17
|
+
} from '@xh/hoist/core';
|
|
18
|
+
import {bindable, makeObservable} from '@xh/hoist/mobx';
|
|
10
19
|
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
11
20
|
import {select} from '@xh/hoist/desktop/cmp/input';
|
|
12
21
|
import '@xh/hoist/desktop/register';
|
|
13
22
|
import {Icon} from '@xh/hoist/icon';
|
|
14
23
|
import {menu, menuDivider, menuItem, popover} from '@xh/hoist/kit/blueprint';
|
|
15
|
-
import {withDefault} from '@xh/hoist/utils/js';
|
|
16
|
-
import {splitLayoutProps} from '@xh/hoist/utils/react';
|
|
24
|
+
import {elemWithin, withDefault} from '@xh/hoist/utils/js';
|
|
25
|
+
import {getLayoutProps, splitLayoutProps} from '@xh/hoist/utils/react';
|
|
17
26
|
import classNames from 'classnames';
|
|
18
27
|
import {isEmpty, sortBy} from 'lodash';
|
|
19
28
|
import {badge} from '@xh/hoist/cmp/badge';
|
|
@@ -41,6 +50,11 @@ export interface FilterChooserProps extends HoistProps<FilterChooserModel>, Layo
|
|
|
41
50
|
placeholder?: string;
|
|
42
51
|
/** Icon clicked to launch favorites menu. (Defaults to Icon.favorite()) */
|
|
43
52
|
favoritesIcon?: ReactElement;
|
|
53
|
+
/**
|
|
54
|
+
* True to render collapsed in-place, expanding into a popover when opened - useful in toolbars
|
|
55
|
+
* and other height-constrained containers. Opens in the direction set by `menuPlacement`.
|
|
56
|
+
*/
|
|
57
|
+
popover?: boolean;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
/**
|
|
@@ -50,6 +64,30 @@ export interface FilterChooserProps extends HoistProps<FilterChooserModel>, Layo
|
|
|
50
64
|
export const [FilterChooser, filterChooser] = hoistCmp.withFactory<FilterChooserProps>({
|
|
51
65
|
model: uses(FilterChooserModel),
|
|
52
66
|
className: 'xh-filter-chooser',
|
|
67
|
+
render({model, className, ...props}, ref) {
|
|
68
|
+
return props.popover
|
|
69
|
+
? popoverFilterChooser({model, className, ...props, ref})
|
|
70
|
+
: filterChooserControl({model, className, ...props, ref});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
//------------------
|
|
75
|
+
// Implementation
|
|
76
|
+
//------------------
|
|
77
|
+
interface FilterChooserControlProps extends FilterChooserProps {
|
|
78
|
+
/** Internal - false to render a non-interactive display (the collapsed popover trigger). */
|
|
79
|
+
xhInteractive?: boolean;
|
|
80
|
+
/** Internal - override for the favorites menu open state. */
|
|
81
|
+
xhFavoritesOpen?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The Select-based control shared by the inline FilterChooser and both faces (collapsed trigger and
|
|
86
|
+
* expanded content) of the popover variant. Only the interactive instance binds `model.inputRef`,
|
|
87
|
+
* so the popover can mount trigger and content simultaneously without contention.
|
|
88
|
+
*/
|
|
89
|
+
const filterChooserControl = hoistCmp.factory<FilterChooserControlProps>({
|
|
90
|
+
model: uses(FilterChooserModel),
|
|
53
91
|
render({model, className, ...props}, ref) {
|
|
54
92
|
const [layoutProps, chooserProps] = splitLayoutProps(props),
|
|
55
93
|
{
|
|
@@ -68,7 +106,9 @@ export const [FilterChooser, filterChooser] = hoistCmp.withFactory<FilterChooser
|
|
|
68
106
|
maxMenuHeight,
|
|
69
107
|
menuPlacement,
|
|
70
108
|
menuWidth,
|
|
71
|
-
favoritesIcon
|
|
109
|
+
favoritesIcon,
|
|
110
|
+
xhInteractive = true,
|
|
111
|
+
xhFavoritesOpen
|
|
72
112
|
} = chooserProps,
|
|
73
113
|
disabled = unsupportedFilter || chooserProps.disabled,
|
|
74
114
|
placeholder = unsupportedFilter
|
|
@@ -90,7 +130,8 @@ export const [FilterChooser, filterChooser] = hoistCmp.withFactory<FilterChooser
|
|
|
90
130
|
flex: 1,
|
|
91
131
|
height: layoutProps?.height,
|
|
92
132
|
bind: 'selectValue',
|
|
93
|
-
|
|
133
|
+
// Only the interactive instance owns the shared inputRef.
|
|
134
|
+
ref: xhInteractive ? inputRef : undefined,
|
|
94
135
|
|
|
95
136
|
autoFocus,
|
|
96
137
|
disabled,
|
|
@@ -119,12 +160,15 @@ export const [FilterChooser, filterChooser] = hoistCmp.withFactory<FilterChooser
|
|
|
119
160
|
},
|
|
120
161
|
components: {
|
|
121
162
|
DropdownIndicator: () => favoritesIconCmp(model, favoritesIcon)
|
|
122
|
-
}
|
|
163
|
+
},
|
|
164
|
+
// Display-only trigger: suppress menu + typing, but leave clear and
|
|
165
|
+
// favorites affordances live for single-click access.
|
|
166
|
+
...(xhInteractive ? {} : {menuIsOpen: false, isSearchable: false})
|
|
123
167
|
}
|
|
124
168
|
})
|
|
125
169
|
),
|
|
126
170
|
content: favoritesMenu(),
|
|
127
|
-
isOpen: favoritesIsOpen,
|
|
171
|
+
isOpen: xhFavoritesOpen ?? favoritesIsOpen,
|
|
128
172
|
position: 'bottom-right',
|
|
129
173
|
minimal: true,
|
|
130
174
|
onInteraction: willOpen => {
|
|
@@ -136,6 +180,102 @@ export const [FilterChooser, filterChooser] = hoistCmp.withFactory<FilterChooser
|
|
|
136
180
|
}
|
|
137
181
|
});
|
|
138
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Wraps a FilterChooser so it renders collapsed in-place and expands into a popover when opened,
|
|
185
|
+
* allowing it to grow vertically beyond the height of a toolbar. The collapsed trigger always
|
|
186
|
+
* occupies its space (so surrounding layout never shifts) and routes single clicks on its clear
|
|
187
|
+
* and favorites controls directly, rather than first opening the popover.
|
|
188
|
+
*/
|
|
189
|
+
const popoverFilterChooser = hoistCmp.factory<FilterChooserProps>({
|
|
190
|
+
model: uses(FilterChooserModel),
|
|
191
|
+
render({model, className, ...props}, ref) {
|
|
192
|
+
const impl = useLocalModel(FilterChooserLocalModel),
|
|
193
|
+
{popoverIsOpen} = impl,
|
|
194
|
+
{popover: _popover, ...rest} = props,
|
|
195
|
+
layoutProps = getLayoutProps(rest);
|
|
196
|
+
|
|
197
|
+
return box({
|
|
198
|
+
ref,
|
|
199
|
+
className: classNames(className, 'xh-filter-chooser--popover'),
|
|
200
|
+
...layoutProps,
|
|
201
|
+
item: popover({
|
|
202
|
+
isOpen: popoverIsOpen,
|
|
203
|
+
popoverClassName: 'xh-filter-chooser__popover',
|
|
204
|
+
matchTargetWidth: true,
|
|
205
|
+
minimal: true,
|
|
206
|
+
position: 'bottom',
|
|
207
|
+
item: filterChooserControl({
|
|
208
|
+
model,
|
|
209
|
+
flex: 1,
|
|
210
|
+
className: 'xh-filter-chooser__trigger',
|
|
211
|
+
...rest,
|
|
212
|
+
displayCount: true,
|
|
213
|
+
xhInteractive: false,
|
|
214
|
+
// Trigger hosts the favorites menu only while collapsed; once open, the
|
|
215
|
+
// expanded content takes over.
|
|
216
|
+
xhFavoritesOpen: model.favoritesIsOpen && !popoverIsOpen
|
|
217
|
+
}),
|
|
218
|
+
content: filterChooserControl({
|
|
219
|
+
model,
|
|
220
|
+
flex: 1,
|
|
221
|
+
className: 'xh-filter-chooser__content',
|
|
222
|
+
...rest,
|
|
223
|
+
displayCount: true
|
|
224
|
+
}),
|
|
225
|
+
onInteraction: (willOpen, e) => {
|
|
226
|
+
if (willOpen) {
|
|
227
|
+
// Let clicks on the inline clear / favorites controls act directly.
|
|
228
|
+
const target = e?.target as HTMLElement;
|
|
229
|
+
if (
|
|
230
|
+
target &&
|
|
231
|
+
(elemWithin(target, 'xh-select__clear-indicator') ||
|
|
232
|
+
elemWithin(target, 'xh-filter-chooser-favorite-icon'))
|
|
233
|
+
) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
impl.open();
|
|
237
|
+
} else {
|
|
238
|
+
impl.close();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
class FilterChooserLocalModel extends HoistModel {
|
|
247
|
+
override xhImpl = true;
|
|
248
|
+
|
|
249
|
+
@lookup(FilterChooserModel)
|
|
250
|
+
model: FilterChooserModel;
|
|
251
|
+
|
|
252
|
+
@bindable
|
|
253
|
+
popoverIsOpen: boolean = false;
|
|
254
|
+
|
|
255
|
+
constructor() {
|
|
256
|
+
super();
|
|
257
|
+
makeObservable(this);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
open() {
|
|
261
|
+
this.popoverIsOpen = true;
|
|
262
|
+
|
|
263
|
+
// Focus the (now-mounted) interactive input and open its menu once available.
|
|
264
|
+
this.addReaction({
|
|
265
|
+
when: () => !!this.model.inputRef.current,
|
|
266
|
+
run: () => {
|
|
267
|
+
const inputRef = this.model.inputRef.current;
|
|
268
|
+
inputRef.focus();
|
|
269
|
+
(inputRef as any).reactSelectRef.current?.openMenu('first');
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
close() {
|
|
275
|
+
this.popoverIsOpen = false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
139
279
|
//-----------------
|
|
140
280
|
// Options
|
|
141
281
|
//------------------
|
|
@@ -5,100 +5,23 @@
|
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {hoistCmp,
|
|
9
|
-
import {bindable, makeObservable} from '@xh/hoist/mobx';
|
|
10
|
-
import {box, hframe} from '@xh/hoist/cmp/layout';
|
|
8
|
+
import {hoistCmp, uses} from '@xh/hoist/core';
|
|
11
9
|
import '@xh/hoist/desktop/register';
|
|
12
|
-
import {popover} from '@xh/hoist/kit/blueprint';
|
|
13
|
-
import {getLayoutProps} from '@xh/hoist/utils/react';
|
|
14
|
-
import './PopoverFilterChooser.scss';
|
|
15
|
-
import {filterChooser, FilterChooserProps} from './FilterChooser';
|
|
16
10
|
import {FilterChooserModel} from '@xh/hoist/cmp/filter';
|
|
11
|
+
import {filterChooser, FilterChooserProps} from './FilterChooser';
|
|
17
12
|
|
|
18
13
|
/**
|
|
19
14
|
* A wrapper around a FilterChooser that renders in a popover when opened, allowing it to expand
|
|
20
15
|
* vertically beyond the height of a toolbar.
|
|
16
|
+
*
|
|
17
|
+
* @deprecated Use `filterChooser({popover: true})` instead - the popover behavior is now a built-in
|
|
18
|
+
* mode of `FilterChooser`. This alias will be removed in a future major release.
|
|
21
19
|
* @see FilterChooser
|
|
22
20
|
*/
|
|
23
21
|
export const [PopoverFilterChooser, popoverFilterChooser] =
|
|
24
22
|
hoistCmp.withFactory<FilterChooserProps>({
|
|
25
23
|
model: uses(FilterChooserModel),
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const layoutProps = getLayoutProps(props),
|
|
29
|
-
impl = useLocalModel(PopoverFilterChooserLocalModel);
|
|
30
|
-
|
|
31
|
-
return box({
|
|
32
|
-
ref,
|
|
33
|
-
className,
|
|
34
|
-
...layoutProps,
|
|
35
|
-
item: popover({
|
|
36
|
-
isOpen: impl.popoverIsOpen,
|
|
37
|
-
popoverClassName: 'xh-popover-filter-chooser__popover',
|
|
38
|
-
item: hframe(
|
|
39
|
-
filterChooser({
|
|
40
|
-
model,
|
|
41
|
-
// Omit when popover is open to force update the inputRef
|
|
42
|
-
omit: impl.popoverIsOpen,
|
|
43
|
-
className: 'xh-popover-filter-chooser__filter-chooser',
|
|
44
|
-
displayCount: true,
|
|
45
|
-
...props,
|
|
46
|
-
disabled: true
|
|
47
|
-
})
|
|
48
|
-
),
|
|
49
|
-
content: filterChooser({
|
|
50
|
-
model,
|
|
51
|
-
displayCount: true,
|
|
52
|
-
...props
|
|
53
|
-
}),
|
|
54
|
-
matchTargetWidth: true,
|
|
55
|
-
minimal: true,
|
|
56
|
-
position: 'bottom',
|
|
57
|
-
onInteraction: open => {
|
|
58
|
-
if (open) {
|
|
59
|
-
impl.open();
|
|
60
|
-
} else {
|
|
61
|
-
impl.close();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
});
|
|
24
|
+
render(props) {
|
|
25
|
+
return filterChooser({popover: true, ...props});
|
|
66
26
|
}
|
|
67
27
|
});
|
|
68
|
-
|
|
69
|
-
class PopoverFilterChooserLocalModel extends HoistModel {
|
|
70
|
-
override xhImpl = true;
|
|
71
|
-
|
|
72
|
-
@lookup(FilterChooserModel)
|
|
73
|
-
model: FilterChooserModel;
|
|
74
|
-
|
|
75
|
-
@bindable
|
|
76
|
-
popoverIsOpen: boolean = false;
|
|
77
|
-
|
|
78
|
-
get displaySelectValue() {
|
|
79
|
-
return this.model.selectValue[0];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
constructor() {
|
|
83
|
-
super();
|
|
84
|
-
makeObservable(this);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
open() {
|
|
88
|
-
this.popoverIsOpen = true;
|
|
89
|
-
|
|
90
|
-
// Focus and open the menu when rendered
|
|
91
|
-
this.addReaction({
|
|
92
|
-
when: () => !!this.model.inputRef.current,
|
|
93
|
-
run: () => {
|
|
94
|
-
const inputRef = this.model.inputRef.current;
|
|
95
|
-
inputRef.focus();
|
|
96
|
-
(inputRef as any).reactSelectRef.current?.openMenu('first');
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
close() {
|
|
102
|
-
this.popoverIsOpen = false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {withDefault} from '@xh/hoist/utils/js';
|
|
8
8
|
import {isNil} from 'lodash';
|
|
9
|
-
import {
|
|
10
|
-
import {CustomCellEditorProps
|
|
9
|
+
import {useEffect} from 'react';
|
|
10
|
+
import {CustomCellEditorProps} from '@xh/hoist/kit/ag-grid';
|
|
11
11
|
import {hoistCmp} from '@xh/hoist/core';
|
|
12
12
|
import {numberInput, NumberInputProps} from '@xh/hoist/desktop/cmp/input';
|
|
13
13
|
import '@xh/hoist/desktop/register';
|
|
@@ -39,20 +39,19 @@ export const [NumberEditor, numberEditor] = hoistCmp.withFactory<NumberEditorPro
|
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (eventKey?.length === 1) onValueChange(eventKey);
|
|
46
|
-
}, [eventKey, onValueChange]);
|
|
47
|
-
|
|
48
|
-
// Gets called before editor component is rendered, to give agGrid a chance to
|
|
49
|
-
// cancel the editing before it even starts if char is not a number.
|
|
50
|
-
const isCancelBeforeStart = useCallback(
|
|
51
|
-
() => eventKey?.length === 1 && '1234567890'.indexOf(eventKey) < 0,
|
|
52
|
-
[eventKey]
|
|
53
|
-
);
|
|
42
|
+
// Characters that can validly begin a numeric entry - digits, sign, and decimal point.
|
|
43
|
+
const NUMBER_START_RE = /[0-9.+-]/;
|
|
54
44
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
45
|
+
const useNumberGuard = ({onValueChange, eventKey, stopEditing}: CustomCellEditorProps) => {
|
|
46
|
+
// When editing is started by typing a printable character, seed the editor with it if it can
|
|
47
|
+
// legally begin a number (digit, `-`, `+`, or `.`), otherwise stop editing to reject the
|
|
48
|
+
// keystroke - reverting to the original value, as the editor was never seeded.
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (eventKey?.length !== 1) return;
|
|
51
|
+
if (NUMBER_START_RE.test(eventKey)) {
|
|
52
|
+
onValueChange(eventKey);
|
|
53
|
+
} else {
|
|
54
|
+
stopEditing(true);
|
|
55
|
+
}
|
|
56
|
+
}, [eventKey, onValueChange, stopEditing]);
|
|
58
57
|
};
|
|
@@ -30,16 +30,21 @@ export const [SelectEditor, selectEditor] = hoistCmp.withFactory<SelectEditorPro
|
|
|
30
30
|
onCommit: flushOnCommit
|
|
31
31
|
? () => wait().then(() => props.agParams.stopEditing())
|
|
32
32
|
: null,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
// Auto-size the menu to content, not the narrow cell - but skip when width is already
|
|
34
|
+
// set, via windowed measurement (#4325) or an explicit `menuWidth` (#4057).
|
|
35
|
+
rsOptions:
|
|
36
|
+
props.inputProps?.enableWindowed || props.inputProps?.menuWidth != null
|
|
37
|
+
? {}
|
|
38
|
+
: {
|
|
39
|
+
styles: {
|
|
40
|
+
menu: styles => ({
|
|
41
|
+
...styles,
|
|
42
|
+
whiteSpace: 'nowrap',
|
|
43
|
+
width: 'auto',
|
|
44
|
+
minWidth: '100%'
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
},
|
|
43
48
|
...props.inputProps
|
|
44
49
|
}
|
|
45
50
|
};
|
|
@@ -35,7 +35,12 @@ export function useInlineEditorModel(
|
|
|
35
35
|
|
|
36
36
|
useGridCellEditor({
|
|
37
37
|
// This is called in full-row editing when the user tabs into the cell
|
|
38
|
-
focusIn: useCallback(() => impl.focus(), [impl])
|
|
38
|
+
focusIn: useCallback(() => impl.focus(), [impl]),
|
|
39
|
+
// Backstop to flush any pending edit when editing ends, in case no blur fired (#4134).
|
|
40
|
+
isCancelAfterEnd: useCallback(() => {
|
|
41
|
+
impl.ref.current?.doCommit();
|
|
42
|
+
return false;
|
|
43
|
+
}, [impl])
|
|
39
44
|
});
|
|
40
45
|
|
|
41
46
|
return component({
|
|
@@ -268,7 +268,6 @@ export class GridFindFieldImplModel extends HoistModel {
|
|
|
268
268
|
|
|
269
269
|
private getActiveFields(): string[] {
|
|
270
270
|
const {gridModel, includeFields, excludeFields} = this,
|
|
271
|
-
groupBy = gridModel.groupBy,
|
|
272
271
|
visibleCols = gridModel.getVisibleLeafColumns();
|
|
273
272
|
|
|
274
273
|
let ret = ['id', ...gridModel.store.fieldNames];
|
|
@@ -293,12 +292,11 @@ export class GridFindFieldImplModel extends HoistModel {
|
|
|
293
292
|
// Run exclude once more to support explicitly excluding a dot-sep field added above.
|
|
294
293
|
if (excludeFields) ret = without(ret, ...excludeFields);
|
|
295
294
|
|
|
296
|
-
// Final filter for column visibility, or explicit request for inclusion.
|
|
295
|
+
// Final filter for column visibility, or explicit request for inclusion. Deliberately not
|
|
296
|
+
// keyed to groupBy, so query results stay stable across regrouping (see #4070).
|
|
297
297
|
ret = ret.filter(f => {
|
|
298
298
|
return (
|
|
299
|
-
(includeFields && includeFields.includes(f)) ||
|
|
300
|
-
visibleCols.find(c => c.field === f) ||
|
|
301
|
-
groupBy.includes(f)
|
|
299
|
+
(includeFields && includeFields.includes(f)) || visibleCols.find(c => c.field === f)
|
|
302
300
|
);
|
|
303
301
|
});
|
|
304
302
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {ColChooserConfig, GridModel} from '@xh/hoist/cmp/grid';
|
|
7
|
+
import {ColChooserConfig, GridModel, IColChooserModel} from '@xh/hoist/cmp/grid';
|
|
8
8
|
import {HoistModel, managed} from '@xh/hoist/core';
|
|
9
9
|
import type {FilterMatchMode} from '@xh/hoist/data';
|
|
10
10
|
import {LeftRightChooserModel} from '@xh/hoist/desktop/cmp/leftrightchooser';
|
|
@@ -15,7 +15,7 @@ import {sortBy} from 'lodash';
|
|
|
15
15
|
* State management for the ColChooser component.
|
|
16
16
|
* @internal
|
|
17
17
|
*/
|
|
18
|
-
export class ColChooserModel extends HoistModel {
|
|
18
|
+
export class ColChooserModel extends HoistModel implements IColChooserModel {
|
|
19
19
|
override xhImpl = true;
|
|
20
20
|
|
|
21
21
|
gridModel: GridModel;
|
|
@@ -55,7 +55,7 @@ const filterForm = hoistCmp.factory(({impl}) => {
|
|
|
55
55
|
fieldDefaults: {label: null, minimal: true},
|
|
56
56
|
item: formField({
|
|
57
57
|
field: 'filter',
|
|
58
|
-
item: jsonInput()
|
|
58
|
+
item: jsonInput({autoFormat: true})
|
|
59
59
|
})
|
|
60
60
|
});
|
|
61
61
|
});
|
|
@@ -147,7 +147,7 @@ class GridFilterDialogLocalModel extends HoistModel {
|
|
|
147
147
|
loadForm() {
|
|
148
148
|
const filter = this.model.filter?.removeFunctionFilters();
|
|
149
149
|
this.formModel.init({
|
|
150
|
-
filter: JSON.stringify(filter
|
|
150
|
+
filter: JSON.stringify(filter ?? null)
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -12,7 +12,7 @@ import {dateInput, numberInput, select, textInput} from '@xh/hoist/desktop/cmp/i
|
|
|
12
12
|
import {Icon} from '@xh/hoist/icon';
|
|
13
13
|
import {kebabCase} from 'lodash';
|
|
14
14
|
|
|
15
|
-
import {CustomRowModel} from './CustomRowModel';
|
|
15
|
+
import {CustomRowModel, usesMultiValueInput} from './CustomRowModel';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Row with operator and value combination for CustomTab.
|
|
@@ -74,17 +74,7 @@ const inputField = hoistCmp.factory<CustomRowModel>(({model}) => {
|
|
|
74
74
|
...fieldSpec.inputProps
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
if (fieldSpec
|
|
78
|
-
return numberInput({
|
|
79
|
-
...props,
|
|
80
|
-
enableShorthandUnits: true
|
|
81
|
-
});
|
|
82
|
-
} else if (fieldSpec.isDateBasedFieldType) {
|
|
83
|
-
return dateInput({
|
|
84
|
-
...props,
|
|
85
|
-
valueType: fieldSpec.fieldType as 'localDate' | 'date'
|
|
86
|
-
});
|
|
87
|
-
} else if (fieldSpec.supportsSuggestions(op as FieldFilterOperator)) {
|
|
77
|
+
if (usesMultiValueInput(fieldSpec, op as FieldFilterOperator)) {
|
|
88
78
|
return select({
|
|
89
79
|
...props,
|
|
90
80
|
options: fieldSpec.values,
|
|
@@ -94,6 +84,16 @@ const inputField = hoistCmp.factory<CustomRowModel>(({model}) => {
|
|
|
94
84
|
hideSelectedOptionCheck: true,
|
|
95
85
|
enableClear: false
|
|
96
86
|
});
|
|
87
|
+
} else if (fieldSpec.isNumericFieldType) {
|
|
88
|
+
return numberInput({
|
|
89
|
+
...props,
|
|
90
|
+
enableShorthandUnits: true
|
|
91
|
+
});
|
|
92
|
+
} else if (fieldSpec.isDateBasedFieldType) {
|
|
93
|
+
return dateInput({
|
|
94
|
+
...props,
|
|
95
|
+
valueType: fieldSpec.fieldType as 'localDate' | 'date'
|
|
96
|
+
});
|
|
97
97
|
} else {
|
|
98
98
|
return textInput(props);
|
|
99
99
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import type {GridFilterFieldSpec} from '@xh/hoist/cmp/grid';
|
|
7
8
|
import {HoistModel} from '@xh/hoist/core';
|
|
8
9
|
import {FieldFilterOperator, FieldFilterSpec} from '@xh/hoist/data';
|
|
9
10
|
import {HeaderFilterModel} from '../HeaderFilterModel';
|
|
@@ -13,6 +14,23 @@ import {CustomTabModel} from './CustomTabModel';
|
|
|
13
14
|
|
|
14
15
|
type OperatorOptionValue = 'blank' | 'not blank' | FieldFilterOperator;
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Whether a row for the given op renders the multi-value `select` input - the only custom-tab input
|
|
19
|
+
* that holds an array of values directly. The single-value inputs (number, date, text) are used
|
|
20
|
+
* otherwise, so array filter values bound for them must be expanded into one row each (see
|
|
21
|
+
* `CustomTabModel.doSyncWithFilter`). Mirrors the input choice made in `CustomRow`.
|
|
22
|
+
*/
|
|
23
|
+
export function usesMultiValueInput(
|
|
24
|
+
fieldSpec: GridFilterFieldSpec,
|
|
25
|
+
op: FieldFilterOperator
|
|
26
|
+
): boolean {
|
|
27
|
+
return (
|
|
28
|
+
!fieldSpec.isNumericFieldType &&
|
|
29
|
+
!fieldSpec.isDateBasedFieldType &&
|
|
30
|
+
fieldSpec.supportsSuggestions(op)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
/**
|
|
17
35
|
* @internal
|
|
18
36
|
*/
|
|
@@ -23,13 +23,24 @@ import {CustomTabModel} from './CustomTabModel';
|
|
|
23
23
|
export const customTab = hoistCmp.factory({
|
|
24
24
|
model: uses(CustomTabModel),
|
|
25
25
|
render({model}) {
|
|
26
|
+
const {rowModels} = model;
|
|
27
|
+
if (!rowModels) {
|
|
28
|
+
return panel({
|
|
29
|
+
className: 'xh-custom-filter-tab',
|
|
30
|
+
item: div({
|
|
31
|
+
className: 'xh-custom-filter-tab__unsupported',
|
|
32
|
+
item: 'This filter is too complex to edit here.'
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
return panel({
|
|
27
38
|
className: 'xh-custom-filter-tab',
|
|
28
39
|
tbar: tbar(),
|
|
29
40
|
items: div({
|
|
30
41
|
className: 'xh-custom-filter-tab__list',
|
|
31
42
|
items: [
|
|
32
|
-
...
|
|
43
|
+
...rowModels.map(m => customRow({model: m})),
|
|
33
44
|
div({
|
|
34
45
|
className: 'xh-custom-filter-tab__list__add-btn-row',
|
|
35
46
|
items: [
|
|
@@ -52,7 +63,7 @@ export const customTab = hoistCmp.factory({
|
|
|
52
63
|
const tbar = hoistCmp.factory<CustomTabModel>(({model}) => {
|
|
53
64
|
return toolbar({
|
|
54
65
|
className: 'xh-custom-filter-tab__tbar',
|
|
55
|
-
omit: model.rowModels.length < 2,
|
|
66
|
+
omit: !model.rowModels || model.rowModels.length < 2,
|
|
56
67
|
compact: true,
|
|
57
68
|
items: [
|
|
58
69
|
filler(),
|