@visns-studio/visns-components 6.4.3 → 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 +21 -21
- package/src/components/columns/NativeDateFilterEditor.jsx +434 -0
- package/src/components/styles/Field.module.scss +25 -3
- package/src/components/styles/global-datagrid.css +141 -0
- package/src/components/styles/global.css +3 -1
- 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
|
@@ -45,7 +45,6 @@ import DatePickerPortal from './utils/DatePickerPortal';
|
|
|
45
45
|
import {
|
|
46
46
|
fromNativeInputValue,
|
|
47
47
|
toNativeInputValue,
|
|
48
|
-
useNativeDateInput,
|
|
49
48
|
} from './utils/nativeDateInput';
|
|
50
49
|
|
|
51
50
|
import 'react-toggle/style.css';
|
|
@@ -171,15 +170,6 @@ function Field({
|
|
|
171
170
|
const [emailSuggestions, setEmailSuggestions] = useState([]);
|
|
172
171
|
const [showEmailSuggestions, setShowEmailSuggestions] = useState(false);
|
|
173
172
|
|
|
174
|
-
/**
|
|
175
|
-
* On touch / narrow devices `date` and `datetime` fields swap
|
|
176
|
-
* react-datepicker's popup calendar for the platform's own picker — the
|
|
177
|
-
* popup is clipped by modals on a phone and its day cells are far below a
|
|
178
|
-
* comfortable tap target. Desktop rendering is untouched.
|
|
179
|
-
* See utils/nativeDateInput.js for the (identical) value contract.
|
|
180
|
-
*/
|
|
181
|
-
const nativeDateInput = useNativeDateInput();
|
|
182
|
-
|
|
183
173
|
/**
|
|
184
174
|
* `colour` field popup picker: dismiss the popover when the user clicks
|
|
185
175
|
* anywhere outside it or presses Escape. Picking a colour inside the
|
|
@@ -3147,16 +3137,22 @@ function Field({
|
|
|
3147
3137
|
}
|
|
3148
3138
|
autoComplete="off"
|
|
3149
3139
|
/>
|
|
3150
|
-
{
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
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>
|
|
3160
3156
|
</div>
|
|
3161
3157
|
);
|
|
3162
3158
|
};
|
|
@@ -3165,7 +3161,11 @@ function Field({
|
|
|
3165
3161
|
switch (settings.type) {
|
|
3166
3162
|
case 'date':
|
|
3167
3163
|
case 'datetime':
|
|
3168
|
-
|
|
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
3169
|
return renderNativeDateInput(settings.type === 'datetime');
|
|
3170
3170
|
}
|
|
3171
3171
|
|
|
@@ -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;
|
|
@@ -1837,9 +1837,19 @@ input.nativeDateInput[type] {
|
|
|
1837
1837
|
input.nativeDateFilled[type] {
|
|
1838
1838
|
padding-right: 2.75rem;
|
|
1839
1839
|
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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;
|
|
1843
1853
|
}
|
|
1844
1854
|
}
|
|
1845
1855
|
|
|
@@ -1848,6 +1858,11 @@ input.nativeDateFilled[type] {
|
|
|
1848
1858
|
top: 50%;
|
|
1849
1859
|
right: 0.4rem;
|
|
1850
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
|
+
}
|
|
1851
1866
|
display: flex;
|
|
1852
1867
|
align-items: center;
|
|
1853
1868
|
justify-content: center;
|
|
@@ -1867,3 +1882,10 @@ input.nativeDateFilled[type] {
|
|
|
1867
1882
|
outline: none;
|
|
1868
1883
|
}
|
|
1869
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);
|
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,
|