@visns-studio/visns-components 6.4.2 → 6.4.4
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 +1 -1
- package/src/components/DataGrid.jsx +16 -1
- package/src/components/Field.jsx +73 -1
- package/src/components/columns/NativeDateFilterEditor.jsx +434 -0
- package/src/components/styles/Field.module.scss +90 -0
- package/src/components/styles/global-datagrid.css +141 -0
- package/src/components/styles/global.css +3 -1
- package/src/components/utils/nativeDateInput.js +172 -0
- package/src/index.js +2 -0
package/package.json
CHANGED
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
92
92
|
},
|
|
93
93
|
"name": "@visns-studio/visns-components",
|
|
94
|
-
"version": "6.4.
|
|
94
|
+
"version": "6.4.4",
|
|
95
95
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
96
96
|
"main": "src/index.js",
|
|
97
97
|
"files": [
|
|
@@ -39,6 +39,7 @@ import NumberFilter from '@visns-studio/visns-datagrid-enterprise/NumberFilter';
|
|
|
39
39
|
import SelectFilter from '@visns-studio/visns-datagrid-enterprise/SelectFilter';
|
|
40
40
|
import DateFilter from '@visns-studio/visns-datagrid-enterprise/DateFilter';
|
|
41
41
|
import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
|
|
42
|
+
import NativeDateFilterEditor from './columns/NativeDateFilterEditor';
|
|
42
43
|
import { toast } from 'react-toastify';
|
|
43
44
|
import fetchUtil from '../utils/fetchUtil';
|
|
44
45
|
import { confirmDialog } from './utils/ConfirmDialog';
|
|
@@ -3725,7 +3726,21 @@ const DataGrid = forwardRef(
|
|
|
3725
3726
|
if (column.filter && column.filter.type) {
|
|
3726
3727
|
switch (column.filter.type) {
|
|
3727
3728
|
case 'date':
|
|
3728
|
-
|
|
3729
|
+
// Native `<input type="date">` by default
|
|
3730
|
+
// — the popup calendar is clipped inside a
|
|
3731
|
+
// scrolling grid and its day cells are far
|
|
3732
|
+
// below a comfortable tap target. A column
|
|
3733
|
+
// can opt back into it with
|
|
3734
|
+
// `filter: { datePicker: 'popup' }`, the
|
|
3735
|
+
// same escape hatch Field uses. Both
|
|
3736
|
+
// editors emit the identical value
|
|
3737
|
+
// (`dateFormat`-formatted string, `''`
|
|
3738
|
+
// when cleared), so the posted `where[]`
|
|
3739
|
+
// entry is unchanged either way.
|
|
3740
|
+
filterEditor =
|
|
3741
|
+
column.filter.datePicker === 'popup'
|
|
3742
|
+
? DateFilter
|
|
3743
|
+
: NativeDateFilterEditor;
|
|
3729
3744
|
filterEditorProps = (props, { index }) => {
|
|
3730
3745
|
return {
|
|
3731
3746
|
dateFormat: 'DD-MM-YYYY',
|
package/src/components/Field.jsx
CHANGED
|
@@ -42,6 +42,10 @@ import VisnsAutocomplete from './Autocomplete';
|
|
|
42
42
|
import VisnsDropZone from './DropZone';
|
|
43
43
|
import MultiCheckbox from './MultiCheckbox';
|
|
44
44
|
import DatePickerPortal from './utils/DatePickerPortal';
|
|
45
|
+
import {
|
|
46
|
+
fromNativeInputValue,
|
|
47
|
+
toNativeInputValue,
|
|
48
|
+
} from './utils/nativeDateInput';
|
|
45
49
|
|
|
46
50
|
import 'react-toggle/style.css';
|
|
47
51
|
import 'react-datepicker/dist/react-datepicker.css';
|
|
@@ -3093,12 +3097,80 @@ function Field({
|
|
|
3093
3097
|
}
|
|
3094
3098
|
};
|
|
3095
3099
|
|
|
3100
|
+
/**
|
|
3101
|
+
* The touch/narrow-viewport rendering of a `date` / `datetime` field: the
|
|
3102
|
+
* platform's own picker instead of react-datepicker's popup calendar.
|
|
3103
|
+
*
|
|
3104
|
+
* It is a drop-in for the DatePicker above — same error class, same
|
|
3105
|
+
* `onChangeDate(value, id)` call, same value shape in form state (a JS
|
|
3106
|
+
* `Date`, or `null` once cleared). The conversions live in
|
|
3107
|
+
* utils/nativeDateInput.js.
|
|
3108
|
+
*
|
|
3109
|
+
* The library's own clear affordance is kept (react-datepicker renders one
|
|
3110
|
+
* via `isClearable`, and iOS offers no way to empty a date input once it
|
|
3111
|
+
* has a value), so the browser's calendar glyph is hidden while it shows —
|
|
3112
|
+
* two clear/open icons side by side would be one too many. Tapping the
|
|
3113
|
+
* field still opens the native picker on every touch platform.
|
|
3114
|
+
*
|
|
3115
|
+
* @param {boolean} withTime `datetime-local` rather than `date`
|
|
3116
|
+
*/
|
|
3117
|
+
const renderNativeDateInput = (withTime) => {
|
|
3118
|
+
const value = toNativeInputValue(inputValue, withTime);
|
|
3119
|
+
|
|
3120
|
+
return (
|
|
3121
|
+
<div className={styles.nativeDateWrap}>
|
|
3122
|
+
<input
|
|
3123
|
+
type={withTime ? 'datetime-local' : 'date'}
|
|
3124
|
+
className={`${styles.nativeDateInput} ${
|
|
3125
|
+
value ? styles.nativeDateFilled : ''
|
|
3126
|
+
} ${inputClass[settings.id] || ''}`}
|
|
3127
|
+
value={value}
|
|
3128
|
+
onChange={(e) =>
|
|
3129
|
+
onChangeDate(
|
|
3130
|
+
fromNativeInputValue(
|
|
3131
|
+
e.target.value,
|
|
3132
|
+
inputValue,
|
|
3133
|
+
withTime
|
|
3134
|
+
),
|
|
3135
|
+
settings.id
|
|
3136
|
+
)
|
|
3137
|
+
}
|
|
3138
|
+
autoComplete="off"
|
|
3139
|
+
/>
|
|
3140
|
+
{/* Always mounted: inserting this node while the input is
|
|
3141
|
+
focused makes mobile browsers dismiss the just-opened
|
|
3142
|
+
native picker (the DOM mutated around the focused
|
|
3143
|
+
control). Visibility is CSS-only. */}
|
|
3144
|
+
<button
|
|
3145
|
+
type="button"
|
|
3146
|
+
aria-label="Clear date"
|
|
3147
|
+
aria-hidden={value ? undefined : 'true'}
|
|
3148
|
+
tabIndex={value ? 0 : -1}
|
|
3149
|
+
className={`${styles.nativeDateClear} ${
|
|
3150
|
+
value ? '' : styles.nativeDateClearHidden
|
|
3151
|
+
}`}
|
|
3152
|
+
onClick={() => onChangeDate(null, settings.id)}
|
|
3153
|
+
>
|
|
3154
|
+
<X size={16} />
|
|
3155
|
+
</button>
|
|
3156
|
+
</div>
|
|
3157
|
+
);
|
|
3158
|
+
};
|
|
3159
|
+
|
|
3096
3160
|
const renderAdditionalContainer = () => {
|
|
3097
3161
|
switch (settings.type) {
|
|
3098
3162
|
case 'date':
|
|
3099
3163
|
case 'datetime':
|
|
3164
|
+
// Native on every viewport — the popup calendar clips inside
|
|
3165
|
+
// modals and duplicates what browsers now do well themselves.
|
|
3166
|
+
// A field can opt back into the react-datepicker popup with
|
|
3167
|
+
// `datePicker: 'popup'` in its config.
|
|
3168
|
+
if (settings.datePicker !== 'popup') {
|
|
3169
|
+
return renderNativeDateInput(settings.type === 'datetime');
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3100
3172
|
return (
|
|
3101
|
-
<DatePickerPortal
|
|
3173
|
+
<DatePickerPortal
|
|
3102
3174
|
containerRef={containerRef}
|
|
3103
3175
|
centerScreen={settings.centerScreen}
|
|
3104
3176
|
modalContext={settings.modalContext}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import moment from 'moment';
|
|
11
|
+
import { X } from 'lucide-react';
|
|
12
|
+
import { toNativeInputValue } from '../utils/nativeDateInput';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Native (`<input type="date">`) replacement for the datagrid's popup
|
|
16
|
+
* calendar column filter.
|
|
17
|
+
*
|
|
18
|
+
* The grid ships `DateFilter` from `@visns-studio/visns-datagrid-*`, which
|
|
19
|
+
* renders an Inovua `DateInput` — a text box plus its own floating calendar.
|
|
20
|
+
* That popup is the last remaining non-native date picker in the app: it is
|
|
21
|
+
* clipped inside scrolling grids, its day cells are far below a comfortable
|
|
22
|
+
* tap target, and it duplicates something every browser now does well. This
|
|
23
|
+
* editor renders the platform control instead, on every viewport.
|
|
24
|
+
*
|
|
25
|
+
* ---------------------------------------------------------------------------
|
|
26
|
+
* The filter-editor contract (community-edition
|
|
27
|
+
* Layout/ColumnLayout/Cell/renderFilter/index.tsx -> `GenericFilter.render`):
|
|
28
|
+
*
|
|
29
|
+
* filterValue { name, operator, type, value, ... } — never mutate it
|
|
30
|
+
* onChange called with a WHOLE filterValue object, `value` replaced
|
|
31
|
+
* emptyValue the filter type's "no filter" value (`''` for `date`)
|
|
32
|
+
* filterDelay ms to debounce before calling onChange (grid default 250)
|
|
33
|
+
* render REQUIRED wrapper — the editor must return `render(node)`,
|
|
34
|
+
* which supplies the filter-row chrome and settings icon
|
|
35
|
+
* cellProps the column's props (may carry `dateFormat`)
|
|
36
|
+
* filterEditorProps object, or `(props, { value, index }) => object`
|
|
37
|
+
* ref receives the editor instance; the grid calls `setValue()`
|
|
38
|
+
* on it (e.g. DataGrid's dependent-filter reset) and reads
|
|
39
|
+
* `getInputRef()`
|
|
40
|
+
* theme / rtl / readOnly / disabled / style / i18n / cell / renderInPortal
|
|
41
|
+
*
|
|
42
|
+
* ---------------------------------------------------------------------------
|
|
43
|
+
* Value shape — byte-identical to `DateFilter`.
|
|
44
|
+
*
|
|
45
|
+
* `DateInput` emits `dateMoment.format(dateFormat)` and `''` for a cleared
|
|
46
|
+
* value (packages/Calendar/src/DateInput/index.tsx `format()`), so with the
|
|
47
|
+
* `dateFormat: 'DD-MM-YYYY'` that DataGrid configures, the grid — and
|
|
48
|
+
* therefore the `where[]` entry posted to the server — has always carried a
|
|
49
|
+
* string like `'02-12-2025'`, and `''` once cleared. This editor formats with
|
|
50
|
+
* the same resolved `dateFormat` and clears to the same `emptyValue`, so no
|
|
51
|
+
* server-side filtering changes.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
// Moment tokens that mean "this format carries a time of day". Escaped
|
|
55
|
+
// literals (`[at]`) are stripped first so they cannot produce a false match.
|
|
56
|
+
const TIME_TOKENS = /[HhmsSAa]/;
|
|
57
|
+
|
|
58
|
+
const formatHasTime = (format) =>
|
|
59
|
+
typeof format === 'string' &&
|
|
60
|
+
TIME_TOKENS.test(format.replace(/\[[^\]]*\]/g, ''));
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the display format the same way `DateFilter` does: an explicit
|
|
64
|
+
* `cellProps.dateFormat` wins, otherwise `filterEditorProps` — which DataGrid
|
|
65
|
+
* passes as a function — is consulted.
|
|
66
|
+
*
|
|
67
|
+
* @returns {string} '' when nothing is configured, which is what
|
|
68
|
+
* `DateInput.format()` falls back on (moment then uses its
|
|
69
|
+
* own ISO default).
|
|
70
|
+
*/
|
|
71
|
+
const resolveDateFormat = (props, value, index) => {
|
|
72
|
+
const { cellProps } = props;
|
|
73
|
+
|
|
74
|
+
if (cellProps && cellProps.dateFormat !== undefined) {
|
|
75
|
+
return cellProps.dateFormat;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return resolveEditorProps(props, value, index).dateFormat || '';
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `filterEditorProps` may be an object or, as DataGrid passes it for date
|
|
83
|
+
* filters, a `(props, { value, index }) => object` factory. DateFilter also
|
|
84
|
+
* falls back to the copy carried on `filterValue`, so this does too.
|
|
85
|
+
*/
|
|
86
|
+
function resolveEditorProps(props, value, index) {
|
|
87
|
+
let { filterEditorProps } = props;
|
|
88
|
+
|
|
89
|
+
if (filterEditorProps === undefined) {
|
|
90
|
+
filterEditorProps = props.filterValue && props.filterValue.filterEditorProps;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof filterEditorProps === 'function') {
|
|
94
|
+
return filterEditorProps(props, { value, index }) || {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return filterEditorProps || {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A stored filter value (formatted with `dateFormat`) -> the string the
|
|
102
|
+
* native input wants ('YYYY-MM-DD' / 'YYYY-MM-DDTHH:mm').
|
|
103
|
+
*/
|
|
104
|
+
const toInputValue = (value, dateFormat, withTime) => {
|
|
105
|
+
if (value === null || value === undefined || value === '') {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Strict against the configured format first — '02-12-2025' parsed
|
|
110
|
+
// loosely would be read as an ISO-ish string and come back wrong.
|
|
111
|
+
let parsed = dateFormat ? moment(value, dateFormat, true) : null;
|
|
112
|
+
|
|
113
|
+
if (!parsed || !parsed.isValid()) {
|
|
114
|
+
parsed = moment(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!parsed.isValid()) {
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return toNativeInputValue(parsed, withTime);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The native input's raw string -> the value the grid and the server expect.
|
|
126
|
+
* Cleared / partially-typed input yields `emptyValue`, which `loadData` in
|
|
127
|
+
* DataGrid.jsx drops from the posted `where[]` so the grid shows every row.
|
|
128
|
+
*/
|
|
129
|
+
const fromInputValue = (raw, dateFormat, withTime, emptyValue) => {
|
|
130
|
+
if (!raw) {
|
|
131
|
+
return emptyValue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// `datetime-local` normally yields 'YYYY-MM-DDTHH:mm', but a non-default
|
|
135
|
+
// `step` makes browsers append seconds — accept both.
|
|
136
|
+
const parsed = withTime
|
|
137
|
+
? moment(raw, ['YYYY-MM-DD[T]HH:mm', 'YYYY-MM-DD[T]HH:mm:ss'], true)
|
|
138
|
+
: moment(raw, 'YYYY-MM-DD', true);
|
|
139
|
+
|
|
140
|
+
if (!parsed.isValid()) {
|
|
141
|
+
return emptyValue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// `format()` with no argument is moment's ISO default — exactly what
|
|
145
|
+
// `DateInput.format()` produces when no dateFormat is configured.
|
|
146
|
+
return dateFormat ? parsed.format(dateFormat) : parsed.format();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const isRangeOperator = (operator) =>
|
|
150
|
+
operator === 'inrange' || operator === 'notinrange';
|
|
151
|
+
|
|
152
|
+
const WRAPPER_CLASS =
|
|
153
|
+
'InovuaReactDataGrid__column-header__filter ' +
|
|
154
|
+
'InovuaReactDataGrid__column-header__filter--date ' +
|
|
155
|
+
'visns-native-date-filter';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* One `<input type="date">` (or `datetime-local`) dressed as a grid filter,
|
|
159
|
+
* with the clear affordance the popup calendar used to provide — Chrome's
|
|
160
|
+
* date input has no clear button of its own.
|
|
161
|
+
*/
|
|
162
|
+
const NativeDateInput = ({
|
|
163
|
+
inputValue,
|
|
164
|
+
withTime,
|
|
165
|
+
disabled,
|
|
166
|
+
readOnly,
|
|
167
|
+
placeholder,
|
|
168
|
+
style,
|
|
169
|
+
onValueChange,
|
|
170
|
+
inputRef,
|
|
171
|
+
...rest
|
|
172
|
+
}) => (
|
|
173
|
+
<span
|
|
174
|
+
className={`${WRAPPER_CLASS}${
|
|
175
|
+
inputValue ? '' : ' visns-native-date-filter--empty'
|
|
176
|
+
}${disabled ? ' visns-native-date-filter--disabled' : ''}`}
|
|
177
|
+
style={{ minWidth: 0, ...style }}
|
|
178
|
+
>
|
|
179
|
+
<input
|
|
180
|
+
{...rest}
|
|
181
|
+
ref={inputRef}
|
|
182
|
+
type={withTime ? 'datetime-local' : 'date'}
|
|
183
|
+
className="visns-native-date-filter__input"
|
|
184
|
+
value={inputValue}
|
|
185
|
+
disabled={disabled}
|
|
186
|
+
readOnly={readOnly}
|
|
187
|
+
// `<input type="date">` has no placeholder of its own — the
|
|
188
|
+
// browser always shows its locale mask — so the configured
|
|
189
|
+
// "Select Date" text becomes the accessible name instead.
|
|
190
|
+
aria-label={placeholder || undefined}
|
|
191
|
+
title={placeholder || undefined}
|
|
192
|
+
autoComplete="off"
|
|
193
|
+
onChange={(event) => onValueChange(event.target.value)}
|
|
194
|
+
/>
|
|
195
|
+
{inputValue && !disabled && !readOnly ? (
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
aria-label="Clear filter"
|
|
199
|
+
className="visns-native-date-filter__clear"
|
|
200
|
+
onClick={() => onValueChange('')}
|
|
201
|
+
>
|
|
202
|
+
<X size={14} />
|
|
203
|
+
</button>
|
|
204
|
+
) : null}
|
|
205
|
+
</span>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const NativeDateFilterEditor = forwardRef((props, ref) => {
|
|
209
|
+
const {
|
|
210
|
+
filterValue,
|
|
211
|
+
onChange,
|
|
212
|
+
filterDelay,
|
|
213
|
+
emptyValue,
|
|
214
|
+
readOnly,
|
|
215
|
+
disabled,
|
|
216
|
+
style,
|
|
217
|
+
render,
|
|
218
|
+
} = props;
|
|
219
|
+
|
|
220
|
+
// `''` is the `date` filter type's emptyValue (community-edition
|
|
221
|
+
// filterTypes.ts) — fall back to it so a grid without the prop still
|
|
222
|
+
// clears to what DateFilter emitted.
|
|
223
|
+
const noValue = emptyValue === undefined ? '' : emptyValue;
|
|
224
|
+
|
|
225
|
+
const range = isRangeOperator(filterValue && filterValue.operator);
|
|
226
|
+
|
|
227
|
+
const [value, setValue] = useState(() =>
|
|
228
|
+
filterValue ? filterValue.value || (range ? {} : '') : ''
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const inputRef = useRef(null);
|
|
232
|
+
const timerRef = useRef(null);
|
|
233
|
+
const propValueRef = useRef(filterValue && filterValue.value);
|
|
234
|
+
|
|
235
|
+
// Keep the emitter fresh for the debounce timer without re-arming it.
|
|
236
|
+
const emitRef = useRef(null);
|
|
237
|
+
emitRef.current = (nextValue) => {
|
|
238
|
+
if (onChange) {
|
|
239
|
+
onChange({ ...filterValue, value: nextValue });
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The popup calendar only ever emitted on an explicit pick, so it never
|
|
245
|
+
* needed `filterDelay`. A native input emits on every segment edit — Chrome
|
|
246
|
+
* reports 0002-12-02, 0020-12-02, 0202-12-02 while a year is typed — so the
|
|
247
|
+
* delay the grid already provides is honoured here, exactly as StringFilter
|
|
248
|
+
* does. The posted value is unchanged; only the moment of the request moves.
|
|
249
|
+
*/
|
|
250
|
+
const emit = useCallback(
|
|
251
|
+
(nextValue) => {
|
|
252
|
+
if (timerRef.current) {
|
|
253
|
+
clearTimeout(timerRef.current);
|
|
254
|
+
timerRef.current = null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (filterDelay && filterDelay >= 1) {
|
|
258
|
+
timerRef.current = setTimeout(() => {
|
|
259
|
+
timerRef.current = null;
|
|
260
|
+
emitRef.current(nextValue);
|
|
261
|
+
}, filterDelay);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
emitRef.current(nextValue);
|
|
266
|
+
},
|
|
267
|
+
[filterDelay]
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
useEffect(
|
|
271
|
+
() => () => {
|
|
272
|
+
if (timerRef.current) {
|
|
273
|
+
clearTimeout(timerRef.current);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
[]
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Mirrors DateFilter.componentDidUpdate: adopt the incoming value only
|
|
280
|
+
// when the grid itself changed it (a reset, a saved view, a dependent
|
|
281
|
+
// filter clear), not on every render.
|
|
282
|
+
const incoming = filterValue ? filterValue.value : undefined;
|
|
283
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (incoming !== propValueRef.current) {
|
|
286
|
+
propValueRef.current = incoming;
|
|
287
|
+
setValue(incoming || (range ? {} : ''));
|
|
288
|
+
}
|
|
289
|
+
}, [incoming, range]);
|
|
290
|
+
|
|
291
|
+
useImperativeHandle(
|
|
292
|
+
ref,
|
|
293
|
+
() => ({
|
|
294
|
+
// GenericFilter -> cellInstance.setFilterValue() reaches the
|
|
295
|
+
// editor through this; like StringFilter/DateFilter it updates
|
|
296
|
+
// the displayed value without re-emitting.
|
|
297
|
+
setValue: (next) => {
|
|
298
|
+
propValueRef.current = next;
|
|
299
|
+
setValue(next || (range ? {} : ''));
|
|
300
|
+
},
|
|
301
|
+
getInputRef: () => inputRef.current,
|
|
302
|
+
}),
|
|
303
|
+
[range]
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const startFormat = useMemo(
|
|
307
|
+
() =>
|
|
308
|
+
resolveDateFormat(
|
|
309
|
+
props,
|
|
310
|
+
range ? value && value.start : value,
|
|
311
|
+
0
|
|
312
|
+
),
|
|
313
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
314
|
+
[props.cellProps, props.filterEditorProps, value, range]
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const endFormat = useMemo(
|
|
318
|
+
() => (range ? resolveDateFormat(props, value && value.end, 1) : ''),
|
|
319
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
320
|
+
[props.cellProps, props.filterEditorProps, value, range]
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const startEditorProps = resolveEditorProps(
|
|
324
|
+
props,
|
|
325
|
+
range ? value && value.start : value,
|
|
326
|
+
0
|
|
327
|
+
);
|
|
328
|
+
const endEditorProps = range
|
|
329
|
+
? resolveEditorProps(props, value && value.end, 1)
|
|
330
|
+
: {};
|
|
331
|
+
|
|
332
|
+
// datetime-local only when the configured format actually carries a time
|
|
333
|
+
// — DataGrid's date filters use 'DD-MM-YYYY', which does not, so they stay
|
|
334
|
+
// date-only and keep emitting a date-only string.
|
|
335
|
+
const startWithTime = formatHasTime(startFormat);
|
|
336
|
+
const endWithTime = formatHasTime(endFormat);
|
|
337
|
+
|
|
338
|
+
const handleChange = useCallback(
|
|
339
|
+
(raw, key, dateFormat, withTime) => {
|
|
340
|
+
const next = fromInputValue(raw, dateFormat, withTime, noValue);
|
|
341
|
+
|
|
342
|
+
if (!key) {
|
|
343
|
+
if (next === value) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
setValue(next);
|
|
348
|
+
propValueRef.current = next;
|
|
349
|
+
emit(next);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const base = value && typeof value === 'object' ? { ...value } : {};
|
|
354
|
+
|
|
355
|
+
if (base[key] === next) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
base[key] = next;
|
|
360
|
+
setValue(base);
|
|
361
|
+
propValueRef.current = base;
|
|
362
|
+
emit(base);
|
|
363
|
+
},
|
|
364
|
+
[emit, noValue, value]
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
if (!filterValue) {
|
|
368
|
+
return render ? render(null) : null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (range) {
|
|
372
|
+
const startValue = value && typeof value === 'object' ? value.start : '';
|
|
373
|
+
const endValue = value && typeof value === 'object' ? value.end : '';
|
|
374
|
+
|
|
375
|
+
const node = (
|
|
376
|
+
<div
|
|
377
|
+
className="visns-native-date-filter__range"
|
|
378
|
+
style={{ display: 'flex', minWidth: 0 }}
|
|
379
|
+
>
|
|
380
|
+
<NativeDateInput
|
|
381
|
+
inputValue={toInputValue(
|
|
382
|
+
startValue,
|
|
383
|
+
startFormat,
|
|
384
|
+
startWithTime
|
|
385
|
+
)}
|
|
386
|
+
withTime={startWithTime}
|
|
387
|
+
disabled={disabled}
|
|
388
|
+
readOnly={readOnly}
|
|
389
|
+
placeholder={startEditorProps.placeholder}
|
|
390
|
+
style={style}
|
|
391
|
+
inputRef={inputRef}
|
|
392
|
+
onValueChange={(raw) =>
|
|
393
|
+
handleChange(raw, 'start', startFormat, startWithTime)
|
|
394
|
+
}
|
|
395
|
+
/>
|
|
396
|
+
<div className="InovuaReactDataGrid__column-header__filter__binary_operator_separator" />
|
|
397
|
+
<NativeDateInput
|
|
398
|
+
inputValue={toInputValue(endValue, endFormat, endWithTime)}
|
|
399
|
+
withTime={endWithTime}
|
|
400
|
+
disabled={disabled}
|
|
401
|
+
readOnly={readOnly}
|
|
402
|
+
placeholder={endEditorProps.placeholder}
|
|
403
|
+
style={style}
|
|
404
|
+
onValueChange={(raw) =>
|
|
405
|
+
handleChange(raw, 'end', endFormat, endWithTime)
|
|
406
|
+
}
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
return render ? render(node) : node;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const node = (
|
|
415
|
+
<NativeDateInput
|
|
416
|
+
inputValue={toInputValue(value, startFormat, startWithTime)}
|
|
417
|
+
withTime={startWithTime}
|
|
418
|
+
disabled={disabled}
|
|
419
|
+
readOnly={readOnly}
|
|
420
|
+
placeholder={startEditorProps.placeholder}
|
|
421
|
+
style={style}
|
|
422
|
+
inputRef={inputRef}
|
|
423
|
+
onValueChange={(raw) =>
|
|
424
|
+
handleChange(raw, null, startFormat, startWithTime)
|
|
425
|
+
}
|
|
426
|
+
/>
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
return render ? render(node) : node;
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
NativeDateFilterEditor.displayName = 'NativeDateFilterEditor';
|
|
433
|
+
|
|
434
|
+
export default NativeDateFilterEditor;
|
|
@@ -1799,3 +1799,93 @@ input[type='file']:hover {
|
|
|
1799
1799
|
min-height: 3lh;
|
|
1800
1800
|
}
|
|
1801
1801
|
}
|
|
1802
|
+
|
|
1803
|
+
/* =========================================================================
|
|
1804
|
+
Native date input (touch / narrow viewports)
|
|
1805
|
+
|
|
1806
|
+
`date` and `datetime` fields swap react-datepicker's popup calendar for
|
|
1807
|
+
the platform picker on phones — see utils/nativeDateInput.js. The box
|
|
1808
|
+
itself is already styled by the global
|
|
1809
|
+
`input[type]:not(...)` rule that every other text field uses, so only the
|
|
1810
|
+
things that rule does not cover live here: the iOS appearance reset, a
|
|
1811
|
+
44px-class tap height, and the clear button that stands in for
|
|
1812
|
+
react-datepicker's `isClearable` cross.
|
|
1813
|
+
========================================================================= */
|
|
1814
|
+
|
|
1815
|
+
.nativeDateWrap {
|
|
1816
|
+
position: relative;
|
|
1817
|
+
width: 100%;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/* `[type]` only to clear the global input rule's specificity — the padding,
|
|
1821
|
+
border, radius and colour all still come from there. */
|
|
1822
|
+
input.nativeDateInput[type] {
|
|
1823
|
+
-webkit-appearance: none;
|
|
1824
|
+
appearance: none;
|
|
1825
|
+
display: block;
|
|
1826
|
+
width: 100%;
|
|
1827
|
+
/* iOS sizes date inputs to their content and centres nothing; a floor
|
|
1828
|
+
keeps the field the same height as its text-input neighbours and
|
|
1829
|
+
comfortably tappable. */
|
|
1830
|
+
min-height: 2.9rem;
|
|
1831
|
+
font-family: inherit;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/* Only once there is a value — and therefore a clear button — does the box
|
|
1835
|
+
need to reserve room on the right, and only then would the browser's own
|
|
1836
|
+
calendar glyph sit under ours. */
|
|
1837
|
+
input.nativeDateFilled[type] {
|
|
1838
|
+
padding-right: 2.75rem;
|
|
1839
|
+
|
|
1840
|
+
/* Phones: tapping the field itself opens the OS sheet, so the browser's
|
|
1841
|
+
calendar glyph is redundant next to our clear button. */
|
|
1842
|
+
@media (pointer: coarse) {
|
|
1843
|
+
&::-webkit-calendar-picker-indicator {
|
|
1844
|
+
-webkit-appearance: none;
|
|
1845
|
+
display: none;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/* Desktop: the indicator IS how the picker opens — keep it visible and
|
|
1850
|
+
reserve room for the clear button to sit beside it. */
|
|
1851
|
+
@media (pointer: fine) {
|
|
1852
|
+
padding-right: 4.4rem;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
.nativeDateClear {
|
|
1857
|
+
position: absolute;
|
|
1858
|
+
top: 50%;
|
|
1859
|
+
right: 0.4rem;
|
|
1860
|
+
transform: translateY(-50%);
|
|
1861
|
+
|
|
1862
|
+
/* Sit left of the browser's calendar indicator on desktop. */
|
|
1863
|
+
@media (pointer: fine) {
|
|
1864
|
+
right: 2.3rem;
|
|
1865
|
+
}
|
|
1866
|
+
display: flex;
|
|
1867
|
+
align-items: center;
|
|
1868
|
+
justify-content: center;
|
|
1869
|
+
width: 2rem;
|
|
1870
|
+
height: 2rem;
|
|
1871
|
+
padding: 0;
|
|
1872
|
+
border: none;
|
|
1873
|
+
background: transparent;
|
|
1874
|
+
border-radius: 50%;
|
|
1875
|
+
cursor: pointer;
|
|
1876
|
+
/* Same token and weight as the datepicker's close icon */
|
|
1877
|
+
color: rgba(var(--primary-rgb, 31, 42, 55), 0.45);
|
|
1878
|
+
|
|
1879
|
+
&:hover,
|
|
1880
|
+
&:focus {
|
|
1881
|
+
color: rgba(var(--primary-rgb, 31, 42, 55), 1);
|
|
1882
|
+
outline: none;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/* Present in the DOM but inert while the field is empty — see the note in
|
|
1887
|
+
Field.jsx's native date markup. */
|
|
1888
|
+
.nativeDateClearHidden {
|
|
1889
|
+
visibility: hidden;
|
|
1890
|
+
pointer-events: none;
|
|
1891
|
+
}
|
|
@@ -510,3 +510,144 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
|
|
|
510
510
|
.InovuaReactDataGrid__group-toolbar {
|
|
511
511
|
display: none !important;
|
|
512
512
|
}
|
|
513
|
+
|
|
514
|
+
/* =========================================================================
|
|
515
|
+
Native date column filters — see columns/NativeDateFilterEditor.jsx
|
|
516
|
+
|
|
517
|
+
The editor replaces Inovua's popup-calendar DateFilter with the platform's
|
|
518
|
+
own picker. Everything here exists to make an `<input type="date">` sit in
|
|
519
|
+
the filter row at exactly the size and weight of its text / dropdown
|
|
520
|
+
neighbours, whose look comes from the toolkit's own
|
|
521
|
+
`.inovua-react-toolkit-text-input--theme-default-light` rule (32px tall,
|
|
522
|
+
1px #cdcecd border, 14px #555e68 text).
|
|
523
|
+
|
|
524
|
+
Height itself is not set here: the wrapper carries the grid's own
|
|
525
|
+
`.InovuaReactDataGrid__column-header__filter` class, which the datagrid
|
|
526
|
+
theme already pins to min/max-height 32px alongside every other filter.
|
|
527
|
+
|
|
528
|
+
Note: `global.css` excludes `.visns-native-date-filter__input` from the
|
|
529
|
+
app-wide `input[type]` rule, whose 1rem padding would otherwise burst the
|
|
530
|
+
32px filter row.
|
|
531
|
+
========================================================================= */
|
|
532
|
+
.visns-native-date-filter {
|
|
533
|
+
position: relative;
|
|
534
|
+
display: flex;
|
|
535
|
+
/* stretch, not center: the input has to fill the 32px the theme pins the
|
|
536
|
+
filter to, or the clickable area is smaller than the box it draws. */
|
|
537
|
+
align-items: stretch;
|
|
538
|
+
box-sizing: border-box;
|
|
539
|
+
width: 100%;
|
|
540
|
+
min-width: 0;
|
|
541
|
+
overflow: hidden;
|
|
542
|
+
border: 1px solid #cdcecd;
|
|
543
|
+
border-radius: 1px;
|
|
544
|
+
background: #fff;
|
|
545
|
+
color: #555e68;
|
|
546
|
+
font-size: 14px;
|
|
547
|
+
transition: border 0.2s, box-shadow 0.2s;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.visns-native-date-filter:hover {
|
|
551
|
+
border-color: #7986cb;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.visns-native-date-filter:focus-within {
|
|
555
|
+
border-color: #7986cb;
|
|
556
|
+
box-shadow: 0 0 0 2px rgba(121, 134, 203, 0.4);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.visns-native-date-filter--disabled {
|
|
560
|
+
opacity: 0.5;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.visns-native-date-filter--disabled:hover {
|
|
564
|
+
border-color: #cdcecd;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.visns-native-date-filter__input {
|
|
568
|
+
flex: 1 1 auto;
|
|
569
|
+
box-sizing: border-box;
|
|
570
|
+
width: 100%;
|
|
571
|
+
min-width: 0;
|
|
572
|
+
height: auto;
|
|
573
|
+
margin: 0;
|
|
574
|
+
padding: 0 2px 0 8px;
|
|
575
|
+
border: none;
|
|
576
|
+
outline: none;
|
|
577
|
+
background: none;
|
|
578
|
+
color: inherit;
|
|
579
|
+
font-family: inherit;
|
|
580
|
+
font-size: inherit;
|
|
581
|
+
line-height: 1;
|
|
582
|
+
-webkit-appearance: none;
|
|
583
|
+
appearance: none;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/* An empty native date input renders its locale mask (dd/mm/yyyy) at full
|
|
587
|
+
contrast, which reads as a value; mute it to the same placeholder weight
|
|
588
|
+
the toolkit's text filters use. (`:invalid` cannot express this — an empty
|
|
589
|
+
optional date input is valid — so the editor sets the modifier itself.) */
|
|
590
|
+
.visns-native-date-filter--empty .visns-native-date-filter__input {
|
|
591
|
+
color: rgba(85, 94, 104, 0.7);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/* The calendar glyph IS how the picker opens on desktop — keep it, just
|
|
595
|
+
shrink it into the 32px row. */
|
|
596
|
+
.visns-native-date-filter__input::-webkit-calendar-picker-indicator {
|
|
597
|
+
margin: 0;
|
|
598
|
+
padding: 0 2px;
|
|
599
|
+
opacity: 0.55;
|
|
600
|
+
cursor: pointer;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.visns-native-date-filter__input::-webkit-calendar-picker-indicator:hover {
|
|
604
|
+
opacity: 1;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* Browser-supplied clear / spin affordances are replaced by our own button,
|
|
608
|
+
which is consistent across engines. */
|
|
609
|
+
.visns-native-date-filter__input::-webkit-clear-button,
|
|
610
|
+
.visns-native-date-filter__input::-webkit-inner-spin-button {
|
|
611
|
+
display: none;
|
|
612
|
+
-webkit-appearance: none;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.visns-native-date-filter__input::-moz-calendar-picker-indicator {
|
|
616
|
+
opacity: 0.55;
|
|
617
|
+
cursor: pointer;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.visns-native-date-filter__clear {
|
|
621
|
+
flex: none;
|
|
622
|
+
align-self: center;
|
|
623
|
+
display: flex;
|
|
624
|
+
align-items: center;
|
|
625
|
+
justify-content: center;
|
|
626
|
+
width: 18px;
|
|
627
|
+
height: 18px;
|
|
628
|
+
margin: 0 4px 0 0;
|
|
629
|
+
padding: 0;
|
|
630
|
+
border: none;
|
|
631
|
+
border-radius: 50%;
|
|
632
|
+
background: transparent;
|
|
633
|
+
color: #99a1aa;
|
|
634
|
+
cursor: pointer;
|
|
635
|
+
outline: none;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.visns-native-date-filter__clear:hover,
|
|
639
|
+
.visns-native-date-filter__clear:focus {
|
|
640
|
+
color: #555e68;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/* inrange / notinrange: two of the same input either side of the theme's
|
|
644
|
+
own separator. */
|
|
645
|
+
.visns-native-date-filter__range {
|
|
646
|
+
flex: 1;
|
|
647
|
+
min-width: 0;
|
|
648
|
+
align-items: center;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.visns-native-date-filter__range .visns-native-date-filter {
|
|
652
|
+
flex: 1 1 0;
|
|
653
|
+
}
|
|
@@ -245,7 +245,9 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
|
|
|
245
245
|
.inovua-react-toolkit-text-input__input
|
|
246
246
|
):not(.inovua-react-toolkit-numeric-input__input):not(
|
|
247
247
|
.variable-inserter-search-input
|
|
248
|
-
):not(.react-datepicker__month-select):not(
|
|
248
|
+
):not(.react-datepicker__month-select):not(
|
|
249
|
+
.react-datepicker__year-select
|
|
250
|
+
):not(.visns-native-date-filter__input) {
|
|
249
251
|
background: white;
|
|
250
252
|
border-radius: var(--br);
|
|
251
253
|
border: 1px solid rgba(var(--paragraph-color-rgb), 0.15);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import moment from 'moment';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Native date input support for touch / narrow devices.
|
|
6
|
+
*
|
|
7
|
+
* react-datepicker's popup calendar is a poor fit for a phone: inside a modal
|
|
8
|
+
* it is clipped by the dialog, the month/year dropdowns get cut off and every
|
|
9
|
+
* day cell is a sub-40px tap target. On those devices the platform's own date
|
|
10
|
+
* picker is both bigger and familiar, so `date` / `datetime` fields render an
|
|
11
|
+
* `<input type="date">` / `<input type="datetime-local">` instead.
|
|
12
|
+
*
|
|
13
|
+
* The value contract is unchanged: whatever the react-datepicker path hands to
|
|
14
|
+
* `Form.handleChangeDate` (a JS `Date`, or `null` when cleared) is exactly what
|
|
15
|
+
* the native path hands it too — see `fromNativeInputValue`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Phones and small tablets in portrait. Anything wider keeps the desktop
|
|
19
|
+
// calendar even if the browser reports a coarse pointer (e.g. a touchscreen
|
|
20
|
+
// laptop is still a mouse-and-keyboard experience for a form).
|
|
21
|
+
export const NATIVE_DATE_MAX_WIDTH = 760;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @returns {boolean} true when the platform date picker should be preferred.
|
|
25
|
+
* SSR-safe: false when there is no `window`.
|
|
26
|
+
*/
|
|
27
|
+
export const prefersNativeDateInput = () => {
|
|
28
|
+
if (typeof window === 'undefined') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const coarsePointer =
|
|
33
|
+
typeof window.matchMedia === 'function' &&
|
|
34
|
+
window.matchMedia('(pointer: coarse)').matches;
|
|
35
|
+
|
|
36
|
+
return coarsePointer || window.innerWidth <= NATIVE_DATE_MAX_WIDTH;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hook form of `prefersNativeDateInput`, re-evaluated on resize / orientation
|
|
41
|
+
* change and on pointer-capability changes (docking a tablet to a mouse).
|
|
42
|
+
*
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
export const useNativeDateInput = () => {
|
|
46
|
+
const [isNative, setIsNative] = useState(prefersNativeDateInput);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (typeof window === 'undefined') {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const update = () => setIsNative(prefersNativeDateInput());
|
|
54
|
+
|
|
55
|
+
window.addEventListener('resize', update);
|
|
56
|
+
|
|
57
|
+
const query =
|
|
58
|
+
typeof window.matchMedia === 'function'
|
|
59
|
+
? window.matchMedia('(pointer: coarse)')
|
|
60
|
+
: null;
|
|
61
|
+
|
|
62
|
+
// Safari < 14 only has the deprecated add/removeListener pair
|
|
63
|
+
if (query) {
|
|
64
|
+
if (typeof query.addEventListener === 'function') {
|
|
65
|
+
query.addEventListener('change', update);
|
|
66
|
+
} else if (typeof query.addListener === 'function') {
|
|
67
|
+
query.addListener(update);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
window.removeEventListener('resize', update);
|
|
73
|
+
|
|
74
|
+
if (query) {
|
|
75
|
+
if (typeof query.removeEventListener === 'function') {
|
|
76
|
+
query.removeEventListener('change', update);
|
|
77
|
+
} else if (typeof query.removeListener === 'function') {
|
|
78
|
+
query.removeListener(update);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
return isNative;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Form state (a `Date`, or a date-ish string on a form that was hydrated from
|
|
89
|
+
* the API before Form.fetchData normalised it) -> the string an
|
|
90
|
+
* `<input type="date">` / `<input type="datetime-local">` expects.
|
|
91
|
+
*
|
|
92
|
+
* @param {Date|string|null|undefined} value
|
|
93
|
+
* @param {boolean} withTime true for `datetime-local`
|
|
94
|
+
* @returns {string} '' when there is nothing valid to show
|
|
95
|
+
*/
|
|
96
|
+
export const toNativeInputValue = (value, withTime = false) => {
|
|
97
|
+
if (
|
|
98
|
+
value === null ||
|
|
99
|
+
value === undefined ||
|
|
100
|
+
value === '' ||
|
|
101
|
+
value === 'null'
|
|
102
|
+
) {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const parsed = moment(value);
|
|
107
|
+
|
|
108
|
+
if (!parsed.isValid()) {
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return parsed.format(
|
|
113
|
+
withTime ? 'YYYY-MM-DD[T]HH:mm' : 'YYYY-MM-DD'
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The native input's raw string -> the value `onChangeDate` already receives
|
|
119
|
+
* from react-datepicker.
|
|
120
|
+
*
|
|
121
|
+
* Two behaviours are copied deliberately from react-datepicker so the two
|
|
122
|
+
* paths cannot drift:
|
|
123
|
+
*
|
|
124
|
+
* - clearing yields `null` (react-datepicker's clear button calls
|
|
125
|
+
* `onChange(null)`), which Form's required-field check reads as empty;
|
|
126
|
+
* - picking a new day on a date-only field keeps the previously selected
|
|
127
|
+
* time of day (react-datepicker's `setSelected` does the same `setTime`
|
|
128
|
+
* on hour/minute/second), so editing the date of a timestamp column does
|
|
129
|
+
* not silently reset it to midnight. Milliseconds are dropped, again as
|
|
130
|
+
* react-datepicker does.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} raw the input element's value
|
|
133
|
+
* @param {Date|string|null} previous the current form value for the field
|
|
134
|
+
* @param {boolean} withTime true for `datetime-local`
|
|
135
|
+
* @returns {Date|null}
|
|
136
|
+
*/
|
|
137
|
+
export const fromNativeInputValue = (raw, previous, withTime = false) => {
|
|
138
|
+
if (!raw) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// `datetime-local` normally yields 'YYYY-MM-DDTHH:mm', but a non-default
|
|
143
|
+
// `step` makes browsers append seconds — accept both.
|
|
144
|
+
const parsed = withTime
|
|
145
|
+
? moment(
|
|
146
|
+
raw,
|
|
147
|
+
['YYYY-MM-DD[T]HH:mm', 'YYYY-MM-DD[T]HH:mm:ss'],
|
|
148
|
+
true
|
|
149
|
+
)
|
|
150
|
+
: moment(raw, 'YYYY-MM-DD', true);
|
|
151
|
+
|
|
152
|
+
if (!parsed.isValid()) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!withTime) {
|
|
157
|
+
const prior = previous ? moment(previous) : null;
|
|
158
|
+
|
|
159
|
+
if (prior && prior.isValid()) {
|
|
160
|
+
parsed.set({
|
|
161
|
+
hour: prior.hour(),
|
|
162
|
+
minute: prior.minute(),
|
|
163
|
+
second: prior.second(),
|
|
164
|
+
millisecond: 0,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return parsed.toDate();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export default useNativeDateInput;
|
package/src/index.js
CHANGED
|
@@ -38,6 +38,7 @@ import AudioPlayer from './components/controls/AudioPlayer';
|
|
|
38
38
|
import GalleryModal from './components/modals/GalleryModal';
|
|
39
39
|
import ImageModal from './components/ImageModal';
|
|
40
40
|
import DatePickerPortal from './components/utils/DatePickerPortal';
|
|
41
|
+
import NativeDateFilterEditor from './components/columns/NativeDateFilterEditor';
|
|
41
42
|
import ImportWizardModal from './components/ImportWizardModal';
|
|
42
43
|
export * from './components/columns/ColumnRenderers.jsx';
|
|
43
44
|
|
|
@@ -147,6 +148,7 @@ export {
|
|
|
147
148
|
MergeEntity,
|
|
148
149
|
MultiSelect,
|
|
149
150
|
MultiCheckbox,
|
|
151
|
+
NativeDateFilterEditor,
|
|
150
152
|
Navigation,
|
|
151
153
|
Notification,
|
|
152
154
|
NotificationList,
|