@xh/hoist 59.2.0 → 59.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/CHANGELOG.md +63 -14
- package/admin/AppComponent.ts +1 -1
- package/admin/tabs/activity/ActivityTab.ts +1 -1
- package/admin/tabs/general/GeneralTab.ts +2 -2
- package/admin/tabs/general/config/ConfigPanel.ts +1 -0
- package/admin/tabs/monitor/MonitorTab.ts +1 -1
- package/admin/tabs/server/ServerTab.ts +1 -1
- package/admin/tabs/userData/UserDataTab.ts +1 -1
- package/cmp/ag-grid/AgGrid.scss +51 -25
- package/cmp/ag-grid/AgGrid.ts +8 -2
- package/cmp/badge/Badge.ts +18 -5
- package/cmp/chart/Chart.ts +13 -11
- package/cmp/clock/Clock.ts +6 -5
- package/cmp/dataview/DataView.ts +5 -3
- package/cmp/form/Form.ts +25 -6
- package/cmp/grid/Grid.ts +41 -25
- package/cmp/grid/GridModel.ts +2 -2
- package/cmp/grid/Types.ts +1 -1
- package/cmp/grid/columns/Column.ts +45 -2
- package/cmp/grid/impl/GridHScrollbar.ts +140 -0
- package/cmp/grid/renderers/MultiFieldRenderer.ts +1 -1
- package/cmp/input/HoistInputModel.ts +4 -4
- package/cmp/input/HoistInputProps.ts +3 -1
- package/cmp/layout/Box.ts +4 -2
- package/cmp/relativetimestamp/RelativeTimestamp.ts +106 -40
- package/cmp/store/StoreFilterField.ts +2 -2
- package/cmp/tab/TabContainer.ts +1 -1
- package/cmp/zoneGrid/Types.ts +47 -0
- package/cmp/zoneGrid/ZoneGrid.ts +62 -0
- package/cmp/zoneGrid/ZoneGridModel.ts +666 -0
- package/cmp/zoneGrid/impl/ZoneGridPersistenceModel.ts +143 -0
- package/cmp/zoneGrid/impl/ZoneMapperModel.ts +335 -0
- package/cmp/zoneGrid/index.ts +3 -0
- package/core/HoistComponent.ts +23 -10
- package/core/HoistProps.ts +25 -6
- package/core/XH.ts +49 -27
- package/core/elem.ts +11 -3
- package/core/impl/InstanceManager.ts +24 -1
- package/core/model/HoistModel.ts +4 -4
- package/data/RecordAction.ts +7 -4
- package/data/StoreRecord.ts +8 -1
- package/desktop/appcontainer/AppContainer.ts +2 -0
- package/desktop/cmp/appbar/AppBar.ts +8 -6
- package/desktop/cmp/button/Button.ts +14 -3
- package/desktop/cmp/button/ButtonGroup.ts +14 -3
- package/desktop/cmp/button/ZoneMapperButton.ts +82 -0
- package/desktop/cmp/button/index.ts +1 -0
- package/desktop/cmp/dash/canvas/DashCanvas.ts +14 -4
- package/desktop/cmp/dash/container/DashContainer.ts +11 -4
- package/desktop/cmp/error/ErrorMessage.ts +9 -8
- package/desktop/cmp/form/FormField.ts +34 -10
- package/desktop/cmp/grid/columns/Actions.ts +2 -1
- package/desktop/cmp/grid/impl/colchooser/ColChooser.ts +3 -2
- package/desktop/cmp/grouping/GroupingChooser.ts +29 -29
- package/desktop/cmp/input/ButtonGroupInput.ts +1 -1
- package/desktop/cmp/input/Checkbox.ts +3 -3
- package/desktop/cmp/input/CodeInput.ts +2 -1
- package/desktop/cmp/input/DateInput.ts +128 -123
- package/desktop/cmp/input/JsonInput.ts +1 -1
- package/desktop/cmp/input/NumberInput.ts +3 -2
- package/desktop/cmp/input/RadioInput.ts +3 -1
- package/desktop/cmp/input/Select.ts +31 -4
- package/desktop/cmp/input/SwitchInput.ts +2 -1
- package/desktop/cmp/input/TextArea.ts +3 -3
- package/desktop/cmp/input/TextInput.ts +51 -47
- package/desktop/cmp/panel/Panel.ts +21 -19
- package/desktop/cmp/panel/impl/ResizeContainer.ts +3 -2
- package/desktop/cmp/pinpad/impl/PinPad.ts +4 -3
- package/desktop/cmp/record/RecordActionBar.ts +12 -3
- package/desktop/cmp/record/impl/RecordActionButton.ts +1 -0
- package/desktop/cmp/rest/Actions.ts +10 -5
- package/desktop/cmp/rest/RestGrid.ts +20 -6
- package/desktop/cmp/rest/impl/RestForm.ts +5 -4
- package/desktop/cmp/rest/impl/RestGridToolbar.ts +3 -2
- package/desktop/cmp/tab/TabSwitcher.ts +8 -3
- package/desktop/cmp/tab/impl/Tab.ts +2 -1
- package/desktop/cmp/tab/impl/TabContainer.ts +18 -15
- package/desktop/cmp/toolbar/Toolbar.ts +3 -1
- package/desktop/cmp/treemap/SplitTreeMap.ts +2 -1
- package/desktop/cmp/treemap/TreeMap.ts +5 -3
- package/desktop/cmp/zoneGrid/impl/ZoneMapper.scss +71 -0
- package/desktop/cmp/zoneGrid/impl/ZoneMapper.ts +232 -0
- package/desktop/cmp/zoneGrid/impl/ZoneMapperDialog.ts +35 -0
- package/dynamics/desktop.ts +2 -0
- package/dynamics/mobile.ts +2 -0
- package/inspector/instances/InstancesModel.ts +2 -2
- package/mobile/appcontainer/AppContainer.ts +2 -0
- package/mobile/cmp/button/ZoneMapperButton.ts +41 -0
- package/mobile/cmp/button/index.ts +1 -0
- package/mobile/cmp/error/ErrorMessage.ts +4 -4
- package/mobile/cmp/input/Select.scss +1 -0
- package/mobile/cmp/input/Select.ts +7 -0
- package/mobile/cmp/input/TextInput.ts +1 -0
- package/mobile/cmp/panel/DialogPanel.scss +18 -6
- package/mobile/cmp/panel/DialogPanel.ts +3 -1
- package/mobile/cmp/zoneGrid/impl/ZoneMapper.scss +67 -0
- package/mobile/cmp/zoneGrid/impl/ZoneMapper.ts +236 -0
- package/package.json +4 -3
- package/styles/vars.scss +3 -3
- package/svc/InspectorService.ts +1 -1
- package/utils/js/DomUtils.ts +10 -0
- package/utils/js/LangUtils.ts +10 -0
- package/utils/js/TestUtils.ts +9 -0
- package/utils/js/index.ts +1 -0
|
@@ -4,22 +4,23 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {PopoverPosition, PopperBoundary} from '@blueprintjs/core';
|
|
7
8
|
import composeRefs from '@seznam/compose-react-refs/composeRefs';
|
|
8
|
-
import {FieldModel, FormContext, FormContextType
|
|
9
|
+
import {BaseFormFieldProps, FieldModel, FormContext, FormContextType} from '@xh/hoist/cmp/form';
|
|
9
10
|
import {box, div, label as labelEl, li, span, ul} from '@xh/hoist/cmp/layout';
|
|
10
11
|
import {DefaultHoistProps, hoistCmp, HSide, uses, XH} from '@xh/hoist/core';
|
|
11
12
|
import '@xh/hoist/desktop/register';
|
|
13
|
+
import {instanceManager} from '@xh/hoist/core/impl/InstanceManager';
|
|
12
14
|
import {fmtDate, fmtDateTime, fmtJson, fmtNumber} from '@xh/hoist/format';
|
|
13
15
|
import {Icon} from '@xh/hoist/icon';
|
|
14
16
|
import {tooltip} from '@xh/hoist/kit/blueprint';
|
|
15
17
|
import {isLocalDate} from '@xh/hoist/utils/datetime';
|
|
16
|
-
import {errorIf, throwIf, withDefault} from '@xh/hoist/utils/js';
|
|
17
|
-
import {getLayoutProps, getReactElementName} from '@xh/hoist/utils/react';
|
|
18
|
+
import {errorIf, getTestId, TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js';
|
|
19
|
+
import {getLayoutProps, getReactElementName, useOnMount, useOnUnmount} from '@xh/hoist/utils/react';
|
|
18
20
|
import classNames from 'classnames';
|
|
19
21
|
import {isBoolean, isDate, isEmpty, isFinite, isNil, isUndefined, kebabCase} from 'lodash';
|
|
20
22
|
import {Children, cloneElement, ReactElement, ReactNode, useContext, useState} from 'react';
|
|
21
23
|
import './FormField.scss';
|
|
22
|
-
import {PopoverPosition, PopperBoundary} from '@blueprintjs/core';
|
|
23
24
|
|
|
24
25
|
export interface FormFieldProps extends BaseFormFieldProps {
|
|
25
26
|
/**
|
|
@@ -162,10 +163,18 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
|
|
|
162
163
|
if (disabled) classes.push('xh-form-field-disabled');
|
|
163
164
|
if (displayNotValid) classes.push('xh-form-field-invalid');
|
|
164
165
|
|
|
166
|
+
const testId = getFormFieldTestId(props, formContext, model.name);
|
|
167
|
+
useOnMount(() => instanceManager.registerModelWithTestId(testId, model));
|
|
168
|
+
useOnUnmount(() => instanceManager.unregisterModelWithTestId(testId));
|
|
169
|
+
|
|
165
170
|
// generate actual element child to render
|
|
166
171
|
let childEl: ReactElement =
|
|
167
172
|
!child || readonly
|
|
168
|
-
? readonlyChild({
|
|
173
|
+
? readonlyChild({
|
|
174
|
+
model,
|
|
175
|
+
readonlyRenderer,
|
|
176
|
+
testId: getTestId(testId, 'readonly-display')
|
|
177
|
+
})
|
|
169
178
|
: editableChild({
|
|
170
179
|
model,
|
|
171
180
|
child,
|
|
@@ -174,7 +183,8 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
|
|
|
174
183
|
disabled,
|
|
175
184
|
displayNotValid,
|
|
176
185
|
leftErrorIcon,
|
|
177
|
-
commitOnChange
|
|
186
|
+
commitOnChange,
|
|
187
|
+
testId: getTestId(testId, 'input')
|
|
178
188
|
});
|
|
179
189
|
|
|
180
190
|
if (minimal) {
|
|
@@ -195,6 +205,7 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
|
|
|
195
205
|
key: model?.xhId,
|
|
196
206
|
className: classNames(className, classes),
|
|
197
207
|
...getLayoutProps(props),
|
|
208
|
+
testId,
|
|
198
209
|
items: [
|
|
199
210
|
labelEl({
|
|
200
211
|
omit: !label,
|
|
@@ -236,9 +247,13 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
|
|
|
236
247
|
const readonlyChild = hoistCmp.factory({
|
|
237
248
|
model: false,
|
|
238
249
|
|
|
239
|
-
render({model, readonlyRenderer}) {
|
|
250
|
+
render({model, readonlyRenderer, testId}) {
|
|
240
251
|
const value = model ? model['value'] : null;
|
|
241
|
-
return div({
|
|
252
|
+
return div({
|
|
253
|
+
className: 'xh-form-field-readonly-display',
|
|
254
|
+
[TEST_ID]: testId,
|
|
255
|
+
item: readonlyRenderer(value)
|
|
256
|
+
});
|
|
242
257
|
}
|
|
243
258
|
});
|
|
244
259
|
|
|
@@ -253,7 +268,8 @@ const editableChild = hoistCmp.factory<FieldModel>({
|
|
|
253
268
|
disabled,
|
|
254
269
|
displayNotValid,
|
|
255
270
|
leftErrorIcon,
|
|
256
|
-
commitOnChange
|
|
271
|
+
commitOnChange,
|
|
272
|
+
testId
|
|
257
273
|
}) {
|
|
258
274
|
const {props} = child;
|
|
259
275
|
|
|
@@ -263,7 +279,8 @@ const editableChild = hoistCmp.factory<FieldModel>({
|
|
|
263
279
|
bind: 'value',
|
|
264
280
|
id: childId,
|
|
265
281
|
disabled: props.disabled || disabled,
|
|
266
|
-
ref: composeRefs(model?.boundInputRef, child.ref)
|
|
282
|
+
ref: composeRefs(model?.boundInputRef, child.ref),
|
|
283
|
+
testId: props.testId ?? testId
|
|
267
284
|
};
|
|
268
285
|
|
|
269
286
|
// If a sizeable child input doesn't specify its own dimensions,
|
|
@@ -347,3 +364,10 @@ function defaultProp(
|
|
|
347
364
|
const fieldDefault = formContext.fieldDefaults ? formContext.fieldDefaults[name] : null;
|
|
348
365
|
return withDefault(props[name], fieldDefault, defaultVal);
|
|
349
366
|
}
|
|
367
|
+
function getFormFieldTestId(
|
|
368
|
+
props: Partial<FormFieldProps>,
|
|
369
|
+
formContext: FormContextType,
|
|
370
|
+
fieldName: string
|
|
371
|
+
): string {
|
|
372
|
+
return props.testId ?? (formContext.testId ? `${formContext.testId}-${fieldName}` : undefined);
|
|
373
|
+
}
|
|
@@ -8,7 +8,7 @@ import {ColumnSpec} from '@xh/hoist/cmp/grid/columns';
|
|
|
8
8
|
import {RecordAction} from '@xh/hoist/data';
|
|
9
9
|
import {button, buttonGroup} from '@xh/hoist/desktop/cmp/button';
|
|
10
10
|
import '@xh/hoist/desktop/register';
|
|
11
|
-
import {throwIf} from '@xh/hoist/utils/js';
|
|
11
|
+
import {getTestId, throwIf} from '@xh/hoist/utils/js';
|
|
12
12
|
import classNames from 'classnames';
|
|
13
13
|
import {isEmpty} from 'lodash';
|
|
14
14
|
|
|
@@ -64,6 +64,7 @@ export const actionCol: ColumnSpec = {
|
|
|
64
64
|
if (hidden) return null;
|
|
65
65
|
|
|
66
66
|
return button({
|
|
67
|
+
testId: getTestId(action.testId, `${record.id}`),
|
|
67
68
|
icon,
|
|
68
69
|
disabled,
|
|
69
70
|
tooltip,
|
|
@@ -43,7 +43,7 @@ export const colChooser = hoistCmp.factory<ColChooserProps>({
|
|
|
43
43
|
filler(),
|
|
44
44
|
button({
|
|
45
45
|
omit: !showRestoreDefaults,
|
|
46
|
-
text: 'Restore
|
|
46
|
+
text: 'Restore Defaults',
|
|
47
47
|
icon: Icon.undo({className: 'xh-red'}),
|
|
48
48
|
onClick: () => model.restoreDefaultsAsync()
|
|
49
49
|
}),
|
|
@@ -57,7 +57,8 @@ export const colChooser = hoistCmp.factory<ColChooserProps>({
|
|
|
57
57
|
button({
|
|
58
58
|
omit: commitOnChange,
|
|
59
59
|
text: 'Save',
|
|
60
|
-
icon: Icon.check(
|
|
60
|
+
icon: Icon.check(),
|
|
61
|
+
intent: 'success',
|
|
61
62
|
onClick: () => {
|
|
62
63
|
model.commit();
|
|
63
64
|
model.close();
|
|
@@ -8,12 +8,13 @@ import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
|
|
|
8
8
|
import {box, div, filler, fragment, hbox, vbox} from '@xh/hoist/cmp/layout';
|
|
9
9
|
import {hoistCmp, uses} from '@xh/hoist/core';
|
|
10
10
|
import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button';
|
|
11
|
-
import {select
|
|
11
|
+
import {select} from '@xh/hoist/desktop/cmp/input';
|
|
12
12
|
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
13
13
|
import '@xh/hoist/desktop/register';
|
|
14
14
|
import {Icon} from '@xh/hoist/icon';
|
|
15
15
|
import {menu, menuDivider, menuItem, popover} from '@xh/hoist/kit/blueprint';
|
|
16
16
|
import {dragDropContext, draggable, droppable} from '@xh/hoist/kit/react-beautiful-dnd';
|
|
17
|
+
import {elemWithin, getTestId, TEST_ID} from '@xh/hoist/utils/js';
|
|
17
18
|
import {splitLayoutProps} from '@xh/hoist/utils/react';
|
|
18
19
|
import classNames from 'classnames';
|
|
19
20
|
import {compact, isEmpty, sortBy} from 'lodash';
|
|
@@ -58,6 +59,7 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
|
|
|
58
59
|
popoverTitle = 'Group By',
|
|
59
60
|
popoverPosition = 'bottom',
|
|
60
61
|
styleButtonAsInput = true,
|
|
62
|
+
testId,
|
|
61
63
|
...rest
|
|
62
64
|
},
|
|
63
65
|
ref
|
|
@@ -65,7 +67,10 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
|
|
|
65
67
|
const {editorIsOpen, favoritesIsOpen, persistFavorites, value, allowEmpty} = model,
|
|
66
68
|
isOpen = editorIsOpen || favoritesIsOpen,
|
|
67
69
|
label = isEmpty(value) && allowEmpty ? emptyText : model.getValueLabel(value),
|
|
68
|
-
[layoutProps, buttonProps] = splitLayoutProps(rest)
|
|
70
|
+
[layoutProps, buttonProps] = splitLayoutProps(rest),
|
|
71
|
+
favoritesMenuTestId = getTestId(testId, 'favorites-menu'),
|
|
72
|
+
favoritesIconTestId = getTestId(testId, 'favorites-icon'),
|
|
73
|
+
editorTestId = getTestId(testId, 'editor');
|
|
69
74
|
|
|
70
75
|
return box({
|
|
71
76
|
ref,
|
|
@@ -91,21 +96,28 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
|
|
|
91
96
|
),
|
|
92
97
|
minimal: styleButtonAsInput,
|
|
93
98
|
...buttonProps,
|
|
94
|
-
onClick: () => model.toggleEditor()
|
|
99
|
+
onClick: () => model.toggleEditor(),
|
|
100
|
+
testId
|
|
95
101
|
}),
|
|
96
|
-
favoritesIcon()
|
|
102
|
+
favoritesIcon({testId: favoritesIconTestId})
|
|
97
103
|
),
|
|
98
104
|
content: favoritesIsOpen
|
|
99
|
-
? favoritesMenu()
|
|
105
|
+
? favoritesMenu({testId: favoritesMenuTestId})
|
|
100
106
|
: editorIsOpen
|
|
101
|
-
? editor({
|
|
107
|
+
? editor({
|
|
108
|
+
popoverWidth,
|
|
109
|
+
popoverMinHeight,
|
|
110
|
+
popoverTitle,
|
|
111
|
+
emptyText,
|
|
112
|
+
testId: editorTestId
|
|
113
|
+
})
|
|
102
114
|
: null,
|
|
103
115
|
onInteraction: (nextOpenState, e) => {
|
|
104
116
|
if (
|
|
105
117
|
isOpen &&
|
|
106
118
|
nextOpenState === false &&
|
|
107
119
|
e?.target &&
|
|
108
|
-
!
|
|
120
|
+
!elemWithin(e.target, 'xh-grouping-chooser-button--with-favorites')
|
|
109
121
|
) {
|
|
110
122
|
model.commitPendingValueAndClose();
|
|
111
123
|
}
|
|
@@ -119,7 +131,7 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
|
|
|
119
131
|
// Editor
|
|
120
132
|
//------------------
|
|
121
133
|
const editor = hoistCmp.factory<GroupingChooserModel>({
|
|
122
|
-
render({popoverWidth, popoverMinHeight, popoverTitle, emptyText}) {
|
|
134
|
+
render({popoverWidth, popoverMinHeight, popoverTitle, emptyText, testId}) {
|
|
123
135
|
return panel({
|
|
124
136
|
width: popoverWidth,
|
|
125
137
|
minHeight: popoverMinHeight,
|
|
@@ -128,7 +140,8 @@ const editor = hoistCmp.factory<GroupingChooserModel>({
|
|
|
128
140
|
dimensionList({emptyText}),
|
|
129
141
|
addDimensionControl(),
|
|
130
142
|
filler()
|
|
131
|
-
]
|
|
143
|
+
],
|
|
144
|
+
testId
|
|
132
145
|
});
|
|
133
146
|
}
|
|
134
147
|
});
|
|
@@ -292,32 +305,16 @@ function getDimOptions(dims, model) {
|
|
|
292
305
|
return sortBy(ret, 'label');
|
|
293
306
|
}
|
|
294
307
|
|
|
295
|
-
function targetIsControlButtonOrPortal(target) {
|
|
296
|
-
const selectPortal = document.getElementById(MENU_PORTAL_ID)?.contains(target),
|
|
297
|
-
selectClick = targetWithin(target, 'xh-select__single-value'),
|
|
298
|
-
editorClick = targetWithin(target, 'xh-grouping-chooser-button--with-favorites');
|
|
299
|
-
return selectPortal || selectClick || editorClick;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Determines whether any of the target's parents have a specific class name
|
|
304
|
-
*/
|
|
305
|
-
function targetWithin(target, className): boolean {
|
|
306
|
-
for (let elem = target; elem; elem = elem.parentElement) {
|
|
307
|
-
if (elem.classList.contains(className)) return true;
|
|
308
|
-
}
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
308
|
//------------------
|
|
313
309
|
// Favorites
|
|
314
310
|
//------------------
|
|
315
311
|
const favoritesIcon = hoistCmp.factory<GroupingChooserModel>({
|
|
316
|
-
render({model}) {
|
|
312
|
+
render({model, testId}) {
|
|
317
313
|
if (!model.persistFavorites) return null;
|
|
318
314
|
return div({
|
|
319
315
|
item: Icon.favorite(),
|
|
320
316
|
className: 'xh-grouping-chooser__favorite-icon',
|
|
317
|
+
[TEST_ID]: testId,
|
|
321
318
|
onClick: e => {
|
|
322
319
|
model.toggleFavoritesMenu();
|
|
323
320
|
e.stopPropagation();
|
|
@@ -327,7 +324,7 @@ const favoritesIcon = hoistCmp.factory<GroupingChooserModel>({
|
|
|
327
324
|
});
|
|
328
325
|
|
|
329
326
|
const favoritesMenu = hoistCmp.factory<GroupingChooserModel>({
|
|
330
|
-
render({model}) {
|
|
327
|
+
render({model, testId}) {
|
|
331
328
|
const options = model.favoritesOptions,
|
|
332
329
|
isFavorite = model.isFavorite(model.value),
|
|
333
330
|
omitAdd = isEmpty(model.value) || isFavorite,
|
|
@@ -349,7 +346,10 @@ const favoritesMenu = hoistCmp.factory<GroupingChooserModel>({
|
|
|
349
346
|
})
|
|
350
347
|
);
|
|
351
348
|
|
|
352
|
-
return vbox(
|
|
349
|
+
return vbox({
|
|
350
|
+
testId,
|
|
351
|
+
items: [div({className: 'xh-popup__title', item: 'Favorites'}), menu({items})]
|
|
352
|
+
});
|
|
353
353
|
}
|
|
354
354
|
});
|
|
355
355
|
|
|
@@ -10,7 +10,7 @@ import {Button, buttonGroup, ButtonGroupProps} from '@xh/hoist/desktop/cmp/butto
|
|
|
10
10
|
import '@xh/hoist/desktop/register';
|
|
11
11
|
import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js';
|
|
12
12
|
import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react';
|
|
13
|
-
import {
|
|
13
|
+
import {castArray, filter, isEmpty, without} from 'lodash';
|
|
14
14
|
import {Children, cloneElement, isValidElement} from 'react';
|
|
15
15
|
|
|
16
16
|
export interface ButtonGroupInputProps
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
|
|
8
|
-
import {hoistCmp,
|
|
8
|
+
import {hoistCmp, HoistProps, HSide, StyleProps} from '@xh/hoist/core';
|
|
9
9
|
import '@xh/hoist/desktop/register';
|
|
10
10
|
import {checkbox as bpCheckbox} from '@xh/hoist/kit/blueprint';
|
|
11
|
-
import {withDefault} from '@xh/hoist/utils/js';
|
|
11
|
+
import {TEST_ID, withDefault} from '@xh/hoist/utils/js';
|
|
12
12
|
import {isNil} from 'lodash';
|
|
13
13
|
import {ReactNode} from 'react';
|
|
14
14
|
|
|
@@ -73,8 +73,8 @@ const cmp = hoistCmp.factory<CheckboxInputModel>(({model, className, ...props},
|
|
|
73
73
|
inline: withDefault(props.inline, true),
|
|
74
74
|
label: props.label,
|
|
75
75
|
tabIndex: props.tabIndex,
|
|
76
|
-
|
|
77
76
|
id: props.id,
|
|
77
|
+
[TEST_ID]: props.testId,
|
|
78
78
|
className,
|
|
79
79
|
style: props.style,
|
|
80
80
|
|
|
@@ -35,8 +35,8 @@ import 'codemirror/addon/selection/mark-selection.js';
|
|
|
35
35
|
import 'codemirror/lib/codemirror.css';
|
|
36
36
|
import 'codemirror/theme/dracula.css';
|
|
37
37
|
import {compact, defaultsDeep, isEqual, isFunction} from 'lodash';
|
|
38
|
-
import {findDOMNode} from 'react-dom';
|
|
39
38
|
import {ReactElement} from 'react';
|
|
39
|
+
import {findDOMNode} from 'react-dom';
|
|
40
40
|
import './CodeInput.scss';
|
|
41
41
|
|
|
42
42
|
export interface CodeInputProps extends HoistProps, HoistInputProps, LayoutProps {
|
|
@@ -455,6 +455,7 @@ const cmp = hoistCmp.factory<CodeInputModel>(({model, className, ...props}, ref)
|
|
|
455
455
|
item: modalSupport({
|
|
456
456
|
model: model.modalSupportModel,
|
|
457
457
|
item: inputCmp({
|
|
458
|
+
testId: props.testId,
|
|
458
459
|
width: '100%',
|
|
459
460
|
height: '100%',
|
|
460
461
|
className,
|
|
@@ -4,28 +4,28 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {PopperBoundary, PopperModifiers} from '@blueprintjs/core';
|
|
8
|
+
import {ITimePickerProps} from '@blueprintjs/datetime';
|
|
7
9
|
import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
|
|
8
10
|
import {div} from '@xh/hoist/cmp/layout';
|
|
9
|
-
import {hoistCmp,
|
|
11
|
+
import {hoistCmp, HoistProps, HSide, LayoutProps, Some} from '@xh/hoist/core';
|
|
10
12
|
import {button, buttonGroup} from '@xh/hoist/desktop/cmp/button';
|
|
11
|
-
import {
|
|
13
|
+
import {textInput, TextInputModel} from '@xh/hoist/desktop/cmp/input';
|
|
12
14
|
import '@xh/hoist/desktop/register';
|
|
13
15
|
import {fmtDate} from '@xh/hoist/format';
|
|
14
16
|
import {Icon} from '@xh/hoist/icon';
|
|
15
17
|
import {datePicker as bpDatePicker, popover, Position} from '@xh/hoist/kit/blueprint';
|
|
16
|
-
import {
|
|
18
|
+
import {bindable, makeObservable} from '@xh/hoist/mobx';
|
|
17
19
|
import {wait} from '@xh/hoist/promise';
|
|
18
20
|
import {isLocalDate, LocalDate} from '@xh/hoist/utils/datetime';
|
|
19
|
-
import {consumeEvent, warnIf, withDefault} from '@xh/hoist/utils/js';
|
|
21
|
+
import {consumeEvent, getTestId, warnIf, withDefault} from '@xh/hoist/utils/js';
|
|
20
22
|
import {getLayoutProps} from '@xh/hoist/utils/react';
|
|
21
23
|
import classNames from 'classnames';
|
|
22
24
|
import {assign, castArray, clone, trim} from 'lodash';
|
|
23
25
|
import moment from 'moment';
|
|
24
26
|
import {createRef, ReactElement, ReactNode} from 'react';
|
|
25
|
-
import './DateInput.scss';
|
|
26
|
-
import {PopperBoundary, PopperModifiers} from '@blueprintjs/core';
|
|
27
|
-
import {ITimePickerProps} from '@blueprintjs/datetime';
|
|
28
27
|
import {DayPickerProps} from 'react-day-picker';
|
|
28
|
+
import './DateInput.scss';
|
|
29
29
|
|
|
30
30
|
export interface DateInputProps extends HoistProps, LayoutProps, HoistInputProps {
|
|
31
31
|
value?: Date | LocalDate;
|
|
@@ -375,123 +375,128 @@ class DateInputModel extends HoistInputModel {
|
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
const cmp = hoistCmp.factory<
|
|
379
|
-
|
|
380
|
-
(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const enablePicker = props.enablePicker ?? true,
|
|
385
|
-
enableTextInput = props.enableTextInput ?? true,
|
|
386
|
-
enableClear = props.enableClear ?? false,
|
|
387
|
-
disabled = props.disabled ?? false,
|
|
388
|
-
isClearable = model.internalValue !== null,
|
|
389
|
-
isOpen = enablePicker && model.popoverOpen && !disabled;
|
|
390
|
-
|
|
391
|
-
const buttons = buttonGroup({
|
|
392
|
-
padding: 0,
|
|
393
|
-
items: [
|
|
394
|
-
button({
|
|
395
|
-
className: 'xh-date-input__clear-icon',
|
|
396
|
-
omit: !enableClear || !isClearable || disabled,
|
|
397
|
-
icon: Icon.cross(),
|
|
398
|
-
tabIndex: -1,
|
|
399
|
-
onClick: model.onClearBtnClick
|
|
400
|
-
}),
|
|
401
|
-
button({
|
|
402
|
-
className: classNames(
|
|
403
|
-
'xh-date-input__picker-icon',
|
|
404
|
-
enablePicker ? null : 'xh-date-input__picker-icon--disabled'
|
|
405
|
-
),
|
|
406
|
-
icon: Icon.calendar(),
|
|
407
|
-
tabIndex: enableTextInput || disabled ? -1 : undefined,
|
|
408
|
-
elementRef: model.buttonRef,
|
|
409
|
-
onClick: enablePicker && !disabled ? model.onOpenPopoverClick : null
|
|
410
|
-
})
|
|
411
|
-
]
|
|
412
|
-
});
|
|
413
|
-
const rightElement = withDefault(props.rightElement, buttons);
|
|
414
|
-
|
|
415
|
-
let {minDate, maxDate, initialMonth, renderValue} = model;
|
|
416
|
-
|
|
417
|
-
// If app has set an out-of-range date, we render it -- these bounds govern *manual* entry
|
|
418
|
-
// But need to relax constraints on the picker, to prevent BP from breaking badly
|
|
419
|
-
if (renderValue) {
|
|
420
|
-
if (minDate && renderValue < minDate) minDate = renderValue;
|
|
421
|
-
if (maxDate && renderValue > maxDate) maxDate = renderValue;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// BP chooses annoying mid-point if forced to guess initial month. Use closest bound instead
|
|
425
|
-
if (!initialMonth && !renderValue) {
|
|
426
|
-
const today = new Date();
|
|
427
|
-
if (minDate && today < minDate) initialMonth = minDate;
|
|
428
|
-
if (maxDate && today > maxDate) initialMonth = maxDate;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
return div({
|
|
432
|
-
className: 'xh-date-input__wrapper',
|
|
433
|
-
item: popover({
|
|
434
|
-
isOpen,
|
|
435
|
-
minimal: true,
|
|
436
|
-
usePortal: true,
|
|
437
|
-
autoFocus: false,
|
|
438
|
-
enforceFocus: false,
|
|
439
|
-
modifiers: props.popoverModifiers,
|
|
440
|
-
position: props.popoverPosition ?? 'auto',
|
|
441
|
-
boundary: props.popoverBoundary ?? 'viewport',
|
|
442
|
-
portalContainer: props.portalContainer ?? document.body,
|
|
443
|
-
popoverRef: model.popoverRef,
|
|
444
|
-
onClose: model.onPopoverClose,
|
|
445
|
-
onInteraction: nextOpenState => {
|
|
446
|
-
if (props.showPickerOnFocus) {
|
|
447
|
-
model.popoverOpen = nextOpenState;
|
|
448
|
-
} else if (!nextOpenState) {
|
|
449
|
-
model.popoverOpen = false;
|
|
450
|
-
}
|
|
451
|
-
},
|
|
452
|
-
|
|
453
|
-
content: bpDatePicker({
|
|
454
|
-
value: renderValue,
|
|
455
|
-
onChange: model.onDatePickerChange,
|
|
456
|
-
maxDate,
|
|
457
|
-
minDate,
|
|
458
|
-
initialMonth,
|
|
459
|
-
showActionsBar: props.showActionsBar,
|
|
460
|
-
dayPickerProps: assign({fixedWeeks: true}, props.dayPickerProps),
|
|
461
|
-
timePrecision: model.timePrecision,
|
|
462
|
-
timePickerProps: model.timePrecision
|
|
463
|
-
? assign({selectAllOnFocus: true}, props.timePickerProps)
|
|
464
|
-
: undefined
|
|
465
|
-
}),
|
|
378
|
+
const cmp = hoistCmp.factory<DateInputProps & {model: DateInputModel}>(
|
|
379
|
+
({model, className, ...props}, ref) => {
|
|
380
|
+
warnIf(
|
|
381
|
+
(props.enableClear || props.enablePicker) && props.rightElement,
|
|
382
|
+
'Cannot specify enableClear or enablePicker along with custom rightElement - built-in clear/picker button will not be shown.'
|
|
383
|
+
);
|
|
466
384
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
385
|
+
const enablePicker = props.enablePicker ?? true,
|
|
386
|
+
enableTextInput = props.enableTextInput ?? true,
|
|
387
|
+
enableClear = props.enableClear ?? false,
|
|
388
|
+
disabled = props.disabled ?? false,
|
|
389
|
+
isClearable = model.internalValue !== null,
|
|
390
|
+
isOpen = enablePicker && model.popoverOpen && !disabled;
|
|
391
|
+
|
|
392
|
+
const buttons = buttonGroup({
|
|
393
|
+
padding: 0,
|
|
394
|
+
items: [
|
|
395
|
+
button({
|
|
396
|
+
className: 'xh-date-input__clear-icon',
|
|
397
|
+
omit: !enableClear || !isClearable || disabled,
|
|
398
|
+
icon: Icon.cross(),
|
|
399
|
+
tabIndex: -1,
|
|
400
|
+
onClick: model.onClearBtnClick,
|
|
401
|
+
testId: getTestId(props, 'clear')
|
|
402
|
+
}),
|
|
403
|
+
button({
|
|
470
404
|
className: classNames(
|
|
471
|
-
|
|
472
|
-
|
|
405
|
+
'xh-date-input__picker-icon',
|
|
406
|
+
enablePicker ? null : 'xh-date-input__picker-icon--disabled'
|
|
473
407
|
),
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
408
|
+
icon: Icon.calendar(),
|
|
409
|
+
tabIndex: enableTextInput || disabled ? -1 : undefined,
|
|
410
|
+
elementRef: model.buttonRef,
|
|
411
|
+
onClick: enablePicker && !disabled ? model.onOpenPopoverClick : null,
|
|
412
|
+
testId: getTestId(props, 'picker')
|
|
413
|
+
})
|
|
414
|
+
]
|
|
415
|
+
});
|
|
416
|
+
const rightElement = withDefault(props.rightElement, buttons);
|
|
417
|
+
|
|
418
|
+
let {minDate, maxDate, initialMonth, renderValue} = model;
|
|
419
|
+
|
|
420
|
+
// If app has set an out-of-range date, we render it -- these bounds govern *manual* entry
|
|
421
|
+
// But need to relax constraints on the picker, to prevent BP from breaking badly
|
|
422
|
+
if (renderValue) {
|
|
423
|
+
if (minDate && renderValue < minDate) minDate = renderValue;
|
|
424
|
+
if (maxDate && renderValue > maxDate) maxDate = renderValue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// BP chooses annoying mid-point if forced to guess initial month. Use closest bound instead
|
|
428
|
+
if (!initialMonth && !renderValue) {
|
|
429
|
+
const today = new Date();
|
|
430
|
+
if (minDate && today < minDate) initialMonth = minDate;
|
|
431
|
+
if (maxDate && today > maxDate) initialMonth = maxDate;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return div({
|
|
435
|
+
className: 'xh-date-input__wrapper',
|
|
436
|
+
item: popover({
|
|
437
|
+
isOpen,
|
|
438
|
+
minimal: true,
|
|
439
|
+
usePortal: true,
|
|
440
|
+
autoFocus: false,
|
|
441
|
+
enforceFocus: false,
|
|
442
|
+
modifiers: props.popoverModifiers,
|
|
443
|
+
position: props.popoverPosition ?? 'auto',
|
|
444
|
+
boundary: props.popoverBoundary ?? 'viewport',
|
|
445
|
+
portalContainer: props.portalContainer ?? document.body,
|
|
446
|
+
popoverRef: model.popoverRef,
|
|
447
|
+
onClose: model.onPopoverClose,
|
|
448
|
+
onInteraction: nextOpenState => {
|
|
449
|
+
if (props.showPickerOnFocus) {
|
|
450
|
+
model.popoverOpen = nextOpenState;
|
|
451
|
+
} else if (!nextOpenState) {
|
|
452
|
+
model.popoverOpen = false;
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
content: bpDatePicker({
|
|
457
|
+
value: renderValue,
|
|
458
|
+
onChange: model.onDatePickerChange,
|
|
459
|
+
maxDate,
|
|
460
|
+
minDate,
|
|
461
|
+
initialMonth,
|
|
462
|
+
showActionsBar: props.showActionsBar,
|
|
463
|
+
dayPickerProps: assign({fixedWeeks: true}, props.dayPickerProps),
|
|
464
|
+
timePrecision: model.timePrecision,
|
|
465
|
+
timePickerProps: model.timePrecision
|
|
466
|
+
? assign({selectAllOnFocus: true}, props.timePickerProps)
|
|
467
|
+
: undefined
|
|
487
468
|
}),
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
469
|
+
|
|
470
|
+
item: div({
|
|
471
|
+
item: textInput({
|
|
472
|
+
value: model.formatDate(renderValue) as string,
|
|
473
|
+
className: classNames(
|
|
474
|
+
className,
|
|
475
|
+
!enableTextInput && !disabled ? 'xh-date-input--picker-only' : null
|
|
476
|
+
),
|
|
477
|
+
onCommit: model.onInputCommit,
|
|
478
|
+
onChange: model.onInputChange,
|
|
479
|
+
onKeyDown: model.onInputKeyDown,
|
|
480
|
+
rightElement,
|
|
481
|
+
disabled: disabled || !enableTextInput,
|
|
482
|
+
leftIcon: props.leftIcon,
|
|
483
|
+
tabIndex: props.tabIndex,
|
|
484
|
+
placeholder: props.placeholder,
|
|
485
|
+
textAlign: props.textAlign,
|
|
486
|
+
selectOnFocus: props.selectOnFocus,
|
|
487
|
+
inputRef: model.inputRef,
|
|
488
|
+
ref: model.textInputRef,
|
|
489
|
+
testId: getTestId(props),
|
|
490
|
+
...getLayoutProps(props)
|
|
491
|
+
}),
|
|
492
|
+
className: 'xh-date-input__click-target',
|
|
493
|
+
onClick: !enableTextInput && !disabled ? model.onOpenPopoverClick : null
|
|
494
|
+
})
|
|
495
|
+
}),
|
|
496
|
+
onBlur: model.onBlur,
|
|
497
|
+
onFocus: model.onFocus,
|
|
498
|
+
onKeyDown: model.onKeyDown,
|
|
499
|
+
ref
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
);
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {hoistCmp} from '@xh/hoist/core';
|
|
8
8
|
import '@xh/hoist/desktop/register';
|
|
9
|
+
import {fmtJson} from '@xh/hoist/format';
|
|
9
10
|
import * as codemirror from 'codemirror';
|
|
10
11
|
import 'codemirror/mode/javascript/javascript';
|
|
11
|
-
import {fmtJson} from '@xh/hoist/format';
|
|
12
12
|
import {codeInput, CodeInputProps} from './CodeInput';
|
|
13
13
|
import {jsonlint} from './impl/jsonlint';
|
|
14
14
|
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import composeRefs from '@seznam/compose-react-refs';
|
|
8
8
|
import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
|
|
9
|
-
import {hoistCmp, HoistProps,
|
|
9
|
+
import {hoistCmp, HoistProps, HSide, LayoutProps, StyleProps} from '@xh/hoist/core';
|
|
10
10
|
import '@xh/hoist/desktop/register';
|
|
11
11
|
import {fmtNumber, parseNumber} from '@xh/hoist/format';
|
|
12
12
|
import {numericInput} from '@xh/hoist/kit/blueprint';
|
|
13
13
|
import {wait} from '@xh/hoist/promise';
|
|
14
|
-
import {apiRemoved, debounced, throwIf, withDefault} from '@xh/hoist/utils/js';
|
|
14
|
+
import {apiRemoved, debounced, TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js';
|
|
15
15
|
import {getLayoutProps} from '@xh/hoist/utils/react';
|
|
16
16
|
import {isNaN, isNil, isNumber, round} from 'lodash';
|
|
17
17
|
import {ReactElement, ReactNode, Ref, useLayoutEffect} from 'react';
|
|
@@ -275,6 +275,7 @@ const cmp = hoistCmp.factory<NumberInputModel>(({model, className, ...props}, re
|
|
|
275
275
|
flex: withDefault(flex, null),
|
|
276
276
|
textAlign: withDefault(props.textAlign, 'right')
|
|
277
277
|
},
|
|
278
|
+
[TEST_ID]: props.testId,
|
|
278
279
|
onBlur: model.onBlur,
|
|
279
280
|
onFocus: model.onFocus,
|
|
280
281
|
onKeyDown: model.onKeyDown,
|