@12-apps/ui 1.5.2 → 1.6.1
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/package.json +4 -4
- package/src/components/data-display/DataViews/DataViewsBoard.tsx +327 -0
- package/src/components/data-display/DataViews/DataViewsGrid.tsx +80 -77
- package/src/components/data-display/DataViews/DataViewsTableBase.tsx +178 -230
- package/src/components/data-display/DataViews/DeleteViewDialog.tsx +83 -0
- package/src/components/data-display/DataViews/SaveViewModal.tsx +140 -52
- package/src/components/data-display/DataViews/data-views-columns-tab.tsx +240 -0
- package/src/components/data-display/DataViews/data-views-columns.ts +129 -0
- package/src/components/data-display/DataViews/data-views-dirty.ts +62 -0
- package/src/components/data-display/DataViews/data-views-display-panel.tsx +328 -0
- package/src/components/data-display/DataViews/data-views-display-tabs.tsx +328 -0
- package/src/components/data-display/DataViews/data-views-empty.tsx +91 -0
- package/src/components/data-display/DataViews/data-views-export.tsx +260 -0
- package/src/components/data-display/DataViews/data-views-glyphs.tsx +103 -0
- package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +340 -0
- package/src/components/data-display/DataViews/data-views-grid-parts.tsx +220 -242
- package/src/components/data-display/DataViews/data-views-inline-bar.tsx +187 -81
- package/src/components/data-display/DataViews/data-views-layout-context.tsx +215 -79
- package/src/components/data-display/DataViews/data-views-more-filters.tsx +300 -0
- package/src/components/data-display/DataViews/data-views-overflow.ts +302 -0
- package/src/components/data-display/DataViews/data-views-preview.ts +11 -3
- package/src/components/data-display/DataViews/data-views-range-pill.tsx +162 -92
- package/src/components/data-display/DataViews/data-views-range-values.ts +69 -0
- package/src/components/data-display/DataViews/data-views-scopes.tsx +356 -0
- package/src/components/data-display/DataViews/data-views-search.tsx +156 -0
- package/src/components/data-display/DataViews/data-views-select-all-strip.tsx +105 -0
- package/src/components/data-display/DataViews/data-views-selection.ts +50 -0
- package/src/components/data-display/DataViews/data-views-server-query.ts +75 -0
- package/src/components/data-display/DataViews/data-views-shell-toolbar.tsx +87 -0
- package/src/components/data-display/DataViews/data-views-table-parts.tsx +5 -0
- package/src/components/data-display/DataViews/data-views-toolbar.tsx +63 -48
- package/src/components/data-display/DataViews/data-views-types.ts +88 -9
- package/src/components/data-display/DataViews/data-views-view-nav.tsx +378 -0
- package/src/components/data-display/DataViews/index.ts +14 -1
- package/src/components/data-display/DataViews/use-data-views-state.ts +158 -134
- package/src/components/data-display/DataViews/use-saved-views-controller.ts +171 -0
- package/src/components/layout/ContentToolbar/ContentToolbar.tsx +86 -30
- package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +25 -0
- package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +6 -1
- package/src/components/utility/InstallPrompt/InstallPrompt.earlyCapture.ts +117 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.md +52 -4
- package/src/components/utility/InstallPrompt/InstallPrompt.types.ts +19 -0
- package/src/components/utility/InstallPrompt/index.ts +8 -0
- package/src/components/utility/InstallPrompt/usePwaInstall.ts +39 -11
|
@@ -11,49 +11,21 @@
|
|
|
11
11
|
* and a page wanting one had to hang its own inputs outside the grid, outside
|
|
12
12
|
* `DataViewState`, and outside the grid's "Limpar filtros" (FUT-668).
|
|
13
13
|
*/
|
|
14
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
14
15
|
import ChevronDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
15
16
|
import { Box, Button, Popover, TextField, Typography } from '@mui/material';
|
|
16
17
|
import React, { useState } from 'react';
|
|
17
18
|
|
|
18
19
|
import { TableFilter } from '../../layout/TableFilter';
|
|
20
|
+
import {
|
|
21
|
+
isRangeInverted,
|
|
22
|
+
isRangeSet,
|
|
23
|
+
numericRange,
|
|
24
|
+
rangeChipLabel,
|
|
25
|
+
} from './data-views-range-values';
|
|
19
26
|
import type { RangeFieldConfig, RangeValue } from './data-views-types';
|
|
20
27
|
|
|
21
|
-
/** Is either bound set? Drives the pill's active styling and the chip. */
|
|
22
|
-
export function isRangeSet(range: RangeValue | undefined): boolean {
|
|
23
|
-
return Boolean(range && (range.min != null || range.max != null));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** `AAAA-MM-DD` → `DD/MM` — the compact form a chip shows. */
|
|
27
|
-
function shortDay(day: string): string {
|
|
28
|
-
const [, month, date] = day.split('-');
|
|
29
|
-
return month && date ? `${date}/${month}` : day;
|
|
30
|
-
}
|
|
31
28
|
|
|
32
|
-
/** One bound, rendered for the chip: money in reais, a day as `DD/MM`. */
|
|
33
|
-
function boundLabel<T extends Record<string, unknown>>(
|
|
34
|
-
field: RangeFieldConfig<T>,
|
|
35
|
-
bound: number | string,
|
|
36
|
-
): string {
|
|
37
|
-
if (field.kind === 'day') return shortDay(String(bound));
|
|
38
|
-
return field.unit ? `${field.unit} ${bound}` : String(bound);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The chip text for an applied range: "Data: 01/07–31/07" when both ends are
|
|
43
|
-
* set, and a one-sided window as the inequality it actually is ("Valor: ≥ R$ 20")
|
|
44
|
-
* rather than an en-dash with a blank side.
|
|
45
|
-
*/
|
|
46
|
-
export function rangeChipLabel<T extends Record<string, unknown>>(
|
|
47
|
-
field: RangeFieldConfig<T>,
|
|
48
|
-
range: RangeValue,
|
|
49
|
-
): string {
|
|
50
|
-
const { min, max } = range;
|
|
51
|
-
if (min != null && max != null) {
|
|
52
|
-
return `${field.label}: ${boundLabel(field, min)}–${boundLabel(field, max)}`;
|
|
53
|
-
}
|
|
54
|
-
if (min != null) return `${field.label}: ≥ ${boundLabel(field, min)}`;
|
|
55
|
-
return `${field.label}: ≤ ${boundLabel(field, max as number | string)}`;
|
|
56
|
-
}
|
|
57
29
|
|
|
58
30
|
/** A bound as the input's string value ('' when unset). */
|
|
59
31
|
function inputValue(bound: number | string | undefined): string {
|
|
@@ -106,6 +78,7 @@ function BoundField<T extends Record<string, unknown>>({
|
|
|
106
78
|
);
|
|
107
79
|
}
|
|
108
80
|
|
|
81
|
+
|
|
109
82
|
/**
|
|
110
83
|
* The `De`/`Até` pair for a range — shared by the pill's popover and the
|
|
111
84
|
* slide-in panel's day fields, so the two surfaces cannot drift on what a bound
|
|
@@ -122,22 +95,46 @@ export function RangeBounds<T extends Record<string, unknown>>({
|
|
|
122
95
|
onChange: (range: RangeValue) => void;
|
|
123
96
|
testId: string;
|
|
124
97
|
}): React.JSX.Element {
|
|
98
|
+
const inverted = isRangeInverted(value);
|
|
125
99
|
return (
|
|
126
|
-
<Box
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
100
|
+
<Box data-testid={`${testId}-panel`}>
|
|
101
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
|
102
|
+
<BoundField
|
|
103
|
+
field={field}
|
|
104
|
+
label="De"
|
|
105
|
+
value={value.min}
|
|
106
|
+
onChange={(min) => onChange({ ...value, min })}
|
|
107
|
+
testId={`${testId}-min`}
|
|
108
|
+
/>
|
|
109
|
+
<BoundField
|
|
110
|
+
field={field}
|
|
111
|
+
label="Até"
|
|
112
|
+
value={value.max}
|
|
113
|
+
onChange={(max) => onChange({ ...value, max })}
|
|
114
|
+
testId={`${testId}-max`}
|
|
115
|
+
/>
|
|
116
|
+
</Box>
|
|
117
|
+
{inverted && (
|
|
118
|
+
<Typography
|
|
119
|
+
component="p"
|
|
120
|
+
data-testid={`${testId}-inverted`}
|
|
121
|
+
sx={{ mt: 0.75, fontSize: '0.75rem', color: 'error.main' }}
|
|
122
|
+
>
|
|
123
|
+
O início precisa ser menor que o fim.
|
|
124
|
+
</Typography>
|
|
125
|
+
)}
|
|
126
|
+
{/* BOTH days are in the result. Saying so is FUT-668's other half: the
|
|
127
|
+
bounds compare as ISO strings precisely so `até` keeps its whole day,
|
|
128
|
+
and nothing on screen would otherwise tell the merchant that. */}
|
|
129
|
+
{field.kind === 'day' && !inverted && (
|
|
130
|
+
<Typography
|
|
131
|
+
component="p"
|
|
132
|
+
data-testid={`${testId}-inclusive`}
|
|
133
|
+
sx={{ mt: 0.75, fontSize: '0.75rem', color: 'text.disabled' }}
|
|
134
|
+
>
|
|
135
|
+
Ambas as datas entram no resultado.
|
|
136
|
+
</Typography>
|
|
137
|
+
)}
|
|
141
138
|
</Box>
|
|
142
139
|
);
|
|
143
140
|
}
|
|
@@ -147,15 +144,116 @@ export function RangeBounds<T extends Record<string, unknown>>({
|
|
|
147
144
|
* each edit is a query) and the popover stays open, which is what lets a
|
|
148
145
|
* merchant set "de" and then "até" without re-opening it.
|
|
149
146
|
*/
|
|
147
|
+
/** An applied range clears from the pill itself — re-opening to empty two fields is three gestures for one intent. */
|
|
148
|
+
function ClearAffordance({
|
|
149
|
+
label,
|
|
150
|
+
onClear,
|
|
151
|
+
testId,
|
|
152
|
+
}: {
|
|
153
|
+
label: string;
|
|
154
|
+
onClear: () => void;
|
|
155
|
+
testId: string;
|
|
156
|
+
}): React.JSX.Element {
|
|
157
|
+
const clear = (event: React.SyntheticEvent): void => {
|
|
158
|
+
event.stopPropagation();
|
|
159
|
+
onClear();
|
|
160
|
+
};
|
|
161
|
+
return (
|
|
162
|
+
<Box
|
|
163
|
+
component="span"
|
|
164
|
+
role="button"
|
|
165
|
+
tabIndex={0}
|
|
166
|
+
aria-label={`Limpar ${label}`}
|
|
167
|
+
data-testid={testId}
|
|
168
|
+
onClick={clear}
|
|
169
|
+
onKeyDown={(event: React.KeyboardEvent) => {
|
|
170
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
clear(event);
|
|
173
|
+
}}
|
|
174
|
+
sx={{ display: 'inline-flex', borderRadius: 999, p: 0.125, '&:hover': { bgcolor: 'action.selected' } }}
|
|
175
|
+
>
|
|
176
|
+
<CloseIcon sx={{ fontSize: 14 }} />
|
|
177
|
+
</Box>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The pill button: its label carries the applied window, and its tone the state. */
|
|
182
|
+
function RangeTrigger<T extends Record<string, unknown>>({
|
|
183
|
+
field,
|
|
184
|
+
value,
|
|
185
|
+
onChange,
|
|
186
|
+
onOpen,
|
|
187
|
+
open,
|
|
188
|
+
testId,
|
|
189
|
+
}: {
|
|
190
|
+
field: RangeFieldConfig<T>;
|
|
191
|
+
value: RangeValue;
|
|
192
|
+
onChange: (range: RangeValue) => void;
|
|
193
|
+
onOpen: (anchor: HTMLElement) => void;
|
|
194
|
+
open: boolean;
|
|
195
|
+
testId: string;
|
|
196
|
+
}): React.JSX.Element {
|
|
197
|
+
const active = isRangeSet(value);
|
|
198
|
+
const inverted = isRangeInverted(value);
|
|
199
|
+
// An INVERTED window gets its own tone rather than the ordinary "applied"
|
|
200
|
+
// one: it is applied, and it matches nothing, and those are different states.
|
|
201
|
+
const tone = inverted ? 'error.main' : active ? 'primary.main' : 'divider';
|
|
202
|
+
return (
|
|
203
|
+
<Button
|
|
204
|
+
variant="outlined"
|
|
205
|
+
color="inherit"
|
|
206
|
+
size="small"
|
|
207
|
+
data-testid={testId}
|
|
208
|
+
aria-haspopup="dialog"
|
|
209
|
+
aria-expanded={open}
|
|
210
|
+
onClick={(event) => onOpen(event.currentTarget)}
|
|
211
|
+
endIcon={
|
|
212
|
+
active ? (
|
|
213
|
+
<ClearAffordance label={field.label} onClear={() => onChange({})} testId={`${testId}-clear-inline`} />
|
|
214
|
+
) : (
|
|
215
|
+
<ChevronDownIcon sx={{ fontSize: 16 }} />
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
sx={{
|
|
219
|
+
borderRadius: 999,
|
|
220
|
+
height: 34,
|
|
221
|
+
px: 1.5,
|
|
222
|
+
maxWidth: 280,
|
|
223
|
+
color: inverted ? 'error.main' : 'text.primary',
|
|
224
|
+
fontWeight: 600,
|
|
225
|
+
fontSize: '0.8125rem',
|
|
226
|
+
textTransform: 'none',
|
|
227
|
+
whiteSpace: 'nowrap',
|
|
228
|
+
borderColor: tone,
|
|
229
|
+
bgcolor: active ? 'action.selected' : 'background.paper',
|
|
230
|
+
'&:hover': {
|
|
231
|
+
borderColor: active ? tone : 'text.primary',
|
|
232
|
+
bgcolor: active ? 'action.selected' : 'action.hover',
|
|
233
|
+
},
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
{/* The applied bounds, in the label — a pill reading only "Valor" hides
|
|
237
|
+
the very thing that is narrowing the list. */}
|
|
238
|
+
<Box component="span" sx={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
239
|
+
{active ? rangeChipLabel(field, value) : field.label}
|
|
240
|
+
</Box>
|
|
241
|
+
</Button>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
150
245
|
export function RangePill<T extends Record<string, unknown>>({
|
|
151
246
|
field,
|
|
152
247
|
value,
|
|
153
248
|
onChange,
|
|
249
|
+
onOpenChange,
|
|
154
250
|
testIdPrefix,
|
|
155
251
|
}: {
|
|
156
252
|
field: RangeFieldConfig<T>;
|
|
157
253
|
value: RangeValue;
|
|
158
254
|
onChange: (range: RangeValue) => void;
|
|
255
|
+
/** So a measured toolbar can hold still while this is open. */
|
|
256
|
+
onOpenChange?: (open: boolean) => void;
|
|
159
257
|
testIdPrefix: string;
|
|
160
258
|
}): React.JSX.Element {
|
|
161
259
|
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
|
@@ -163,37 +261,24 @@ export function RangePill<T extends Record<string, unknown>>({
|
|
|
163
261
|
const testId = `${testIdPrefix}-range-${field.id}`;
|
|
164
262
|
return (
|
|
165
263
|
<>
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
onClick={(event) => setAnchor(event.currentTarget)}
|
|
174
|
-
endIcon={<ChevronDownIcon sx={{ fontSize: 16 }} />}
|
|
175
|
-
sx={{
|
|
176
|
-
borderRadius: 999,
|
|
177
|
-
height: 34,
|
|
178
|
-
px: 1.5,
|
|
179
|
-
color: 'text.primary',
|
|
180
|
-
fontWeight: 600,
|
|
181
|
-
fontSize: '0.8125rem',
|
|
182
|
-
textTransform: 'none',
|
|
183
|
-
borderColor: active ? 'primary.main' : 'divider',
|
|
184
|
-
bgcolor: active ? 'action.selected' : 'background.paper',
|
|
185
|
-
'&:hover': {
|
|
186
|
-
borderColor: active ? 'primary.main' : 'text.primary',
|
|
187
|
-
bgcolor: active ? 'action.selected' : 'action.hover',
|
|
188
|
-
},
|
|
264
|
+
<RangeTrigger
|
|
265
|
+
field={field}
|
|
266
|
+
value={value}
|
|
267
|
+
onChange={onChange}
|
|
268
|
+
onOpen={(element) => {
|
|
269
|
+
setAnchor(element);
|
|
270
|
+
onOpenChange?.(true);
|
|
189
271
|
}}
|
|
190
|
-
|
|
191
|
-
{
|
|
192
|
-
|
|
272
|
+
open={Boolean(anchor)}
|
|
273
|
+
testId={testId}
|
|
274
|
+
/>
|
|
193
275
|
<Popover
|
|
194
276
|
open={Boolean(anchor)}
|
|
195
277
|
anchorEl={anchor}
|
|
196
|
-
onClose={() =>
|
|
278
|
+
onClose={() => {
|
|
279
|
+
setAnchor(null);
|
|
280
|
+
onOpenChange?.(false);
|
|
281
|
+
}}
|
|
197
282
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
|
198
283
|
slotProps={{ paper: { sx: { p: 2, mt: 0.5 } } }}
|
|
199
284
|
>
|
|
@@ -222,21 +307,6 @@ export function RangePill<T extends Record<string, unknown>>({
|
|
|
222
307
|
);
|
|
223
308
|
}
|
|
224
309
|
|
|
225
|
-
/**
|
|
226
|
-
* A stored range narrowed to NUMBERS, for the slide-in panel's numeric control.
|
|
227
|
-
* A bound that doesn't parse is dropped rather than passed on as `NaN`, which
|
|
228
|
-
* the control would render as an empty box that still filtered.
|
|
229
|
-
*/
|
|
230
|
-
export function numericRange(range: RangeValue): { min?: number; max?: number } {
|
|
231
|
-
const parse = (bound: number | string | undefined): number | undefined => {
|
|
232
|
-
if (bound == null || bound === "") return undefined;
|
|
233
|
-
const value = Number(bound);
|
|
234
|
-
return Number.isNaN(value) ? undefined : value;
|
|
235
|
-
};
|
|
236
|
-
const min = parse(range.min);
|
|
237
|
-
const max = parse(range.max);
|
|
238
|
-
return { ...(min !== undefined ? { min } : {}), ...(max !== undefined ? { max } : {}) };
|
|
239
|
-
}
|
|
240
310
|
|
|
241
311
|
/**
|
|
242
312
|
* One range on the SLIDE-IN panel (and the narrow-screen modal): the numeric
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { RangeFieldConfig, RangeValue } from "./data-views-types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a range VALUE means, separate from the controls that edit one.
|
|
5
|
+
*
|
|
6
|
+
* Pure and component-free: whether a window is set, whether it is inverted, how
|
|
7
|
+
* it reads as a label, and its numeric form. The pill, the overflow panel and
|
|
8
|
+
* the filter panel all ask these questions, and none of them should have to
|
|
9
|
+
* import a component to do it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Is either bound set? Drives the pill's active styling and the chip. */
|
|
13
|
+
export function isRangeSet(range: RangeValue | undefined): boolean {
|
|
14
|
+
return Boolean(range && (range.min != null || range.max != null));
|
|
15
|
+
}
|
|
16
|
+
/** `AAAA-MM-DD` → `DD/MM` — the compact form a chip shows. */
|
|
17
|
+
function shortDay(day: string): string {
|
|
18
|
+
const [, month, date] = day.split('-');
|
|
19
|
+
return month && date ? `${date}/${month}` : day;
|
|
20
|
+
}
|
|
21
|
+
/** One bound, rendered for the chip: money in reais, a day as `DD/MM`. */
|
|
22
|
+
function boundLabel<T extends Record<string, unknown>>(
|
|
23
|
+
field: RangeFieldConfig<T>,
|
|
24
|
+
bound: number | string,
|
|
25
|
+
): string {
|
|
26
|
+
if (field.kind === 'day') return shortDay(String(bound));
|
|
27
|
+
return field.unit ? `${field.unit} ${bound}` : String(bound);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The chip text for an applied range: "Data: 01/07–31/07" when both ends are
|
|
31
|
+
* set, and a one-sided window as the inequality it actually is ("Valor: ≥ R$ 20")
|
|
32
|
+
* rather than an en-dash with a blank side.
|
|
33
|
+
*/
|
|
34
|
+
export function rangeChipLabel<T extends Record<string, unknown>>(
|
|
35
|
+
field: RangeFieldConfig<T>,
|
|
36
|
+
range: RangeValue,
|
|
37
|
+
): string {
|
|
38
|
+
const { min, max } = range;
|
|
39
|
+
if (min != null && max != null) {
|
|
40
|
+
return `${field.label}: ${boundLabel(field, min)}–${boundLabel(field, max)}`;
|
|
41
|
+
}
|
|
42
|
+
if (min != null) return `${field.label}: ≥ ${boundLabel(field, min)}`;
|
|
43
|
+
return `${field.label}: ≤ ${boundLabel(field, max as number | string)}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Is the window INVERTED — a `de` after its `até`?
|
|
47
|
+
*
|
|
48
|
+
* Not an error the merchant must clear before the list responds: it simply
|
|
49
|
+
* matches nothing. But an empty grid with no explanation reads as "there is no
|
|
50
|
+
* data", so the control says which of the two it is.
|
|
51
|
+
*/
|
|
52
|
+
export function isRangeInverted(value: RangeValue): boolean {
|
|
53
|
+
return value.min != null && value.max != null && value.min > value.max;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A stored range narrowed to NUMBERS, for the slide-in panel's numeric control.
|
|
57
|
+
* A bound that doesn't parse is dropped rather than passed on as `NaN`, which
|
|
58
|
+
* the control would render as an empty box that still filtered.
|
|
59
|
+
*/
|
|
60
|
+
export function numericRange(range: RangeValue): { min?: number; max?: number } {
|
|
61
|
+
const parse = (bound: number | string | undefined): number | undefined => {
|
|
62
|
+
if (bound == null || bound === "") return undefined;
|
|
63
|
+
const value = Number(bound);
|
|
64
|
+
return Number.isNaN(value) ? undefined : value;
|
|
65
|
+
};
|
|
66
|
+
const min = parse(range.min);
|
|
67
|
+
const max = parse(range.max);
|
|
68
|
+
return { ...(min !== undefined ? { min } : {}), ...(max !== undefined ? { max } : {}) };
|
|
69
|
+
}
|