@flytedan/flytebot-design-system 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +157 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -4
- package/dist/index.d.ts +27 -4
- package/dist/index.js +157 -61
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.d.cts
CHANGED
|
@@ -1138,6 +1138,10 @@ interface FilterFieldRange extends FilterFieldBase {
|
|
|
1138
1138
|
min?: number;
|
|
1139
1139
|
max?: number;
|
|
1140
1140
|
step?: number;
|
|
1141
|
+
/** Requires whole numbers for both bounds. Off by default, the same opt-in NumberInput
|
|
1142
|
+
* itself takes — a range field with a whole-number step (e.g. 10) may still want
|
|
1143
|
+
* decimal bounds, so this is a deliberate choice, not inferred from `step`. */
|
|
1144
|
+
integer?: boolean;
|
|
1141
1145
|
minLabel?: string;
|
|
1142
1146
|
maxLabel?: string;
|
|
1143
1147
|
minPlaceholder?: string;
|
|
@@ -1154,11 +1158,15 @@ type FilterDateValue = {
|
|
|
1154
1158
|
interface FilterFieldDate extends FilterFieldBase {
|
|
1155
1159
|
type: "date";
|
|
1156
1160
|
/** Default days shown the first time "within the last N days" is picked — shown, not
|
|
1157
|
-
* written: nothing is set until the user changes it or confirms it.
|
|
1161
|
+
* written: nothing is set until the user changes it or confirms it. Must be a whole
|
|
1162
|
+
* number from 1 to `maxDays`; FilterBar throws a clear, field-naming error in
|
|
1163
|
+
* development if it isn't. */
|
|
1158
1164
|
defaultDays?: number;
|
|
1159
1165
|
/** Upper bound enforced on the "within the last N days" input. Default 3650 (ten
|
|
1160
1166
|
* years) — generous enough for any real "recent activity" window while still
|
|
1161
|
-
* guaranteeing a plausible, in-range value ever reaches `onFilterChange`.
|
|
1167
|
+
* guaranteeing a plausible, in-range value ever reaches `onFilterChange`. Must be a
|
|
1168
|
+
* whole number from 1 to 3650; FilterBar throws a clear, field-naming error in
|
|
1169
|
+
* development if it isn't. */
|
|
1162
1170
|
maxDays?: number;
|
|
1163
1171
|
}
|
|
1164
1172
|
/** A declared filter field. Add a variant here (and a branch below) for a new value
|
|
@@ -2192,7 +2200,10 @@ interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement
|
|
|
2192
2200
|
}
|
|
2193
2201
|
declare function Textarea({ label, help, error, required, rows, disabled, id, className, style, ...rest }: TextareaProps): React.JSX.Element;
|
|
2194
2202
|
|
|
2195
|
-
/** Numeric field with thousands formatting on blur, +/- steppers with hold-to-repeat
|
|
2203
|
+
/** Numeric field with thousands formatting on blur, +/- steppers with hold-to-repeat
|
|
2204
|
+
* acceleration and arrow keys (shift = bigStep) that snap to the step grid within
|
|
2205
|
+
* [min, max], and inline min/max/integer validation that keeps an invalid typed value
|
|
2206
|
+
* from ever reaching `onChange`. */
|
|
2196
2207
|
interface NumberInputProps {
|
|
2197
2208
|
label?: string;
|
|
2198
2209
|
help?: string;
|
|
@@ -2214,6 +2225,12 @@ interface NumberInputProps {
|
|
|
2214
2225
|
step?: number;
|
|
2215
2226
|
/** Larger step used while holding the steppers or shift+arrows. */
|
|
2216
2227
|
bigStep?: number;
|
|
2228
|
+
/** Requires a whole number. Off by default — a field with a whole-number `step` (e.g.
|
|
2229
|
+
* cents in steps of 10) may still want decimal input, so this is a deliberate opt-in
|
|
2230
|
+
* rather than inferred from `step`. When set, typed text, blur, the steppers and arrow
|
|
2231
|
+
* keys all reject or snap away from a fractional value; a fractional value can never
|
|
2232
|
+
* reach `onChange`. */
|
|
2233
|
+
integer?: boolean;
|
|
2217
2234
|
/** Thousands-format the display when not editing. Default true. */
|
|
2218
2235
|
format?: boolean;
|
|
2219
2236
|
value?: number | string;
|
|
@@ -2222,11 +2239,17 @@ interface NumberInputProps {
|
|
|
2222
2239
|
* (e.g. FilterBar's range/date panels) tells an unset bound apart from a typed
|
|
2223
2240
|
* zero by this, not by testing for `0` itself. */
|
|
2224
2241
|
onChange?: (e: any) => void;
|
|
2242
|
+
/** Called with the field's current validity every time it changes — true once the
|
|
2243
|
+
* typed text is empty or a legal number, false while it is out of bounds, non-integer
|
|
2244
|
+
* (when `integer` is set), or otherwise unparseable. Lets a parent (e.g. a panel with
|
|
2245
|
+
* its own Done button) know a value is mid-edit-and-invalid even though nothing has
|
|
2246
|
+
* been (or ever will be) passed to `onChange` for it. */
|
|
2247
|
+
onValidityChange?: (valid: boolean) => void;
|
|
2225
2248
|
placeholder?: string;
|
|
2226
2249
|
style?: any;
|
|
2227
2250
|
className?: string;
|
|
2228
2251
|
}
|
|
2229
|
-
declare function NumberInput({ label, help, error, prefix, suffix, required, disabled, min, max, step, bigStep, value, onChange, format, className, style, placeholder, id, "aria-label": ariaLabel }: NumberInputProps): React.JSX.Element;
|
|
2252
|
+
declare function NumberInput({ label, help, error, prefix, suffix, required, disabled, min, max, step, bigStep, integer, value, onChange, onValidityChange, format, className, style, placeholder, id, "aria-label": ariaLabel }: NumberInputProps): React.JSX.Element;
|
|
2230
2253
|
|
|
2231
2254
|
/** Rich popover select: animated listbox, keyboard nav + type-ahead, search over 8 options, option icons, descriptions, meta and groups. API-compatible with the old native select. */
|
|
2232
2255
|
interface SelectOption {
|
package/dist/index.d.ts
CHANGED
|
@@ -1138,6 +1138,10 @@ interface FilterFieldRange extends FilterFieldBase {
|
|
|
1138
1138
|
min?: number;
|
|
1139
1139
|
max?: number;
|
|
1140
1140
|
step?: number;
|
|
1141
|
+
/** Requires whole numbers for both bounds. Off by default, the same opt-in NumberInput
|
|
1142
|
+
* itself takes — a range field with a whole-number step (e.g. 10) may still want
|
|
1143
|
+
* decimal bounds, so this is a deliberate choice, not inferred from `step`. */
|
|
1144
|
+
integer?: boolean;
|
|
1141
1145
|
minLabel?: string;
|
|
1142
1146
|
maxLabel?: string;
|
|
1143
1147
|
minPlaceholder?: string;
|
|
@@ -1154,11 +1158,15 @@ type FilterDateValue = {
|
|
|
1154
1158
|
interface FilterFieldDate extends FilterFieldBase {
|
|
1155
1159
|
type: "date";
|
|
1156
1160
|
/** Default days shown the first time "within the last N days" is picked — shown, not
|
|
1157
|
-
* written: nothing is set until the user changes it or confirms it.
|
|
1161
|
+
* written: nothing is set until the user changes it or confirms it. Must be a whole
|
|
1162
|
+
* number from 1 to `maxDays`; FilterBar throws a clear, field-naming error in
|
|
1163
|
+
* development if it isn't. */
|
|
1158
1164
|
defaultDays?: number;
|
|
1159
1165
|
/** Upper bound enforced on the "within the last N days" input. Default 3650 (ten
|
|
1160
1166
|
* years) — generous enough for any real "recent activity" window while still
|
|
1161
|
-
* guaranteeing a plausible, in-range value ever reaches `onFilterChange`.
|
|
1167
|
+
* guaranteeing a plausible, in-range value ever reaches `onFilterChange`. Must be a
|
|
1168
|
+
* whole number from 1 to 3650; FilterBar throws a clear, field-naming error in
|
|
1169
|
+
* development if it isn't. */
|
|
1162
1170
|
maxDays?: number;
|
|
1163
1171
|
}
|
|
1164
1172
|
/** A declared filter field. Add a variant here (and a branch below) for a new value
|
|
@@ -2192,7 +2200,10 @@ interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement
|
|
|
2192
2200
|
}
|
|
2193
2201
|
declare function Textarea({ label, help, error, required, rows, disabled, id, className, style, ...rest }: TextareaProps): React.JSX.Element;
|
|
2194
2202
|
|
|
2195
|
-
/** Numeric field with thousands formatting on blur, +/- steppers with hold-to-repeat
|
|
2203
|
+
/** Numeric field with thousands formatting on blur, +/- steppers with hold-to-repeat
|
|
2204
|
+
* acceleration and arrow keys (shift = bigStep) that snap to the step grid within
|
|
2205
|
+
* [min, max], and inline min/max/integer validation that keeps an invalid typed value
|
|
2206
|
+
* from ever reaching `onChange`. */
|
|
2196
2207
|
interface NumberInputProps {
|
|
2197
2208
|
label?: string;
|
|
2198
2209
|
help?: string;
|
|
@@ -2214,6 +2225,12 @@ interface NumberInputProps {
|
|
|
2214
2225
|
step?: number;
|
|
2215
2226
|
/** Larger step used while holding the steppers or shift+arrows. */
|
|
2216
2227
|
bigStep?: number;
|
|
2228
|
+
/** Requires a whole number. Off by default — a field with a whole-number `step` (e.g.
|
|
2229
|
+
* cents in steps of 10) may still want decimal input, so this is a deliberate opt-in
|
|
2230
|
+
* rather than inferred from `step`. When set, typed text, blur, the steppers and arrow
|
|
2231
|
+
* keys all reject or snap away from a fractional value; a fractional value can never
|
|
2232
|
+
* reach `onChange`. */
|
|
2233
|
+
integer?: boolean;
|
|
2217
2234
|
/** Thousands-format the display when not editing. Default true. */
|
|
2218
2235
|
format?: boolean;
|
|
2219
2236
|
value?: number | string;
|
|
@@ -2222,11 +2239,17 @@ interface NumberInputProps {
|
|
|
2222
2239
|
* (e.g. FilterBar's range/date panels) tells an unset bound apart from a typed
|
|
2223
2240
|
* zero by this, not by testing for `0` itself. */
|
|
2224
2241
|
onChange?: (e: any) => void;
|
|
2242
|
+
/** Called with the field's current validity every time it changes — true once the
|
|
2243
|
+
* typed text is empty or a legal number, false while it is out of bounds, non-integer
|
|
2244
|
+
* (when `integer` is set), or otherwise unparseable. Lets a parent (e.g. a panel with
|
|
2245
|
+
* its own Done button) know a value is mid-edit-and-invalid even though nothing has
|
|
2246
|
+
* been (or ever will be) passed to `onChange` for it. */
|
|
2247
|
+
onValidityChange?: (valid: boolean) => void;
|
|
2225
2248
|
placeholder?: string;
|
|
2226
2249
|
style?: any;
|
|
2227
2250
|
className?: string;
|
|
2228
2251
|
}
|
|
2229
|
-
declare function NumberInput({ label, help, error, prefix, suffix, required, disabled, min, max, step, bigStep, value, onChange, format, className, style, placeholder, id, "aria-label": ariaLabel }: NumberInputProps): React.JSX.Element;
|
|
2252
|
+
declare function NumberInput({ label, help, error, prefix, suffix, required, disabled, min, max, step, bigStep, integer, value, onChange, onValidityChange, format, className, style, placeholder, id, "aria-label": ariaLabel }: NumberInputProps): React.JSX.Element;
|
|
2230
2253
|
|
|
2231
2254
|
/** Rich popover select: animated listbox, keyboard nav + type-ahead, search over 8 options, option icons, descriptions, meta and groups. API-compatible with the old native select. */
|
|
2232
2255
|
interface SelectOption {
|
package/dist/index.js
CHANGED
|
@@ -2769,8 +2769,10 @@ function NumberInput({
|
|
|
2769
2769
|
max = Infinity,
|
|
2770
2770
|
step = 1,
|
|
2771
2771
|
bigStep,
|
|
2772
|
+
integer = false,
|
|
2772
2773
|
value,
|
|
2773
2774
|
onChange,
|
|
2775
|
+
onValidityChange,
|
|
2774
2776
|
format = true,
|
|
2775
2777
|
className = "",
|
|
2776
2778
|
style,
|
|
@@ -2786,9 +2788,8 @@ function NumberInput({
|
|
|
2786
2788
|
const n = Number(s);
|
|
2787
2789
|
return isNaN(n) ? null : n;
|
|
2788
2790
|
};
|
|
2789
|
-
const requiresInteger = Number.isInteger(step);
|
|
2790
2791
|
const validationMessage = (n) => {
|
|
2791
|
-
if (
|
|
2792
|
+
if (integer && !Number.isInteger(n)) return "Enter a whole number";
|
|
2792
2793
|
if (n < min) return "Must be \u2265 " + min;
|
|
2793
2794
|
if (n > max) return "Must be \u2264 " + max;
|
|
2794
2795
|
return null;
|
|
@@ -2803,11 +2804,17 @@ function NumberInput({
|
|
|
2803
2804
|
setInvalidMessage(null);
|
|
2804
2805
|
}
|
|
2805
2806
|
}, [value, editing]);
|
|
2807
|
+
React21.useEffect(() => {
|
|
2808
|
+
onValidityChange && onValidityChange(invalidMessage == null);
|
|
2809
|
+
}, [invalidMessage]);
|
|
2806
2810
|
const clamp = (n) => Math.min(max, Math.max(min, n));
|
|
2807
2811
|
const fire = (n) => onChange && onChange({ target: { value: n } });
|
|
2808
2812
|
const bump = (dir, mult = 1) => {
|
|
2809
|
-
const cur = num(text) ?? num(value) ?? (min === -Infinity ? 0 : min);
|
|
2810
|
-
const
|
|
2813
|
+
const cur = (invalidMessage ? null : num(text)) ?? num(value) ?? (min === -Infinity ? 0 : min);
|
|
2814
|
+
const amount = (bigStep && mult > 1 ? bigStep : step) * (bigStep ? 1 : mult);
|
|
2815
|
+
let next = cur + dir * amount;
|
|
2816
|
+
if (integer) next = Math.round(next);
|
|
2817
|
+
next = clamp(next);
|
|
2811
2818
|
setInvalidMessage(null);
|
|
2812
2819
|
setText(String(next));
|
|
2813
2820
|
fire(next);
|
|
@@ -3194,6 +3201,91 @@ function isFieldSet(field, value) {
|
|
|
3194
3201
|
}
|
|
3195
3202
|
}
|
|
3196
3203
|
}
|
|
3204
|
+
function validateFilterValue(field, value) {
|
|
3205
|
+
switch (field.type) {
|
|
3206
|
+
case "text":
|
|
3207
|
+
case "select":
|
|
3208
|
+
return { ok: true };
|
|
3209
|
+
case "multiSelect": {
|
|
3210
|
+
const v = value;
|
|
3211
|
+
if (!v || !Array.isArray(v.values) || v.values.length === 0) {
|
|
3212
|
+
return { ok: false, reason: "Select at least one value" };
|
|
3213
|
+
}
|
|
3214
|
+
if (!operatorsFor(field).includes(v.operator)) {
|
|
3215
|
+
return { ok: false, reason: "That operator isn't allowed for " + field.label };
|
|
3216
|
+
}
|
|
3217
|
+
return { ok: true };
|
|
3218
|
+
}
|
|
3219
|
+
case "range": {
|
|
3220
|
+
const v = value;
|
|
3221
|
+
if (!v || v.min == null && v.max == null) {
|
|
3222
|
+
return { ok: false, reason: "Enter at least one bound" };
|
|
3223
|
+
}
|
|
3224
|
+
const lo = field.min ?? -Infinity;
|
|
3225
|
+
const hi = field.max ?? Infinity;
|
|
3226
|
+
for (const n of [v.min, v.max]) {
|
|
3227
|
+
if (n == null) continue;
|
|
3228
|
+
if (field.integer && !Number.isInteger(n)) return { ok: false, reason: "Enter a whole number" };
|
|
3229
|
+
if (n < lo) return { ok: false, reason: "Must be \u2265 " + lo };
|
|
3230
|
+
if (n > hi) return { ok: false, reason: "Must be \u2264 " + hi };
|
|
3231
|
+
}
|
|
3232
|
+
if (v.min != null && v.max != null && v.min > v.max) {
|
|
3233
|
+
return { ok: false, reason: "Must be \u2265 " + v.min };
|
|
3234
|
+
}
|
|
3235
|
+
return { ok: true };
|
|
3236
|
+
}
|
|
3237
|
+
case "date": {
|
|
3238
|
+
const v = value;
|
|
3239
|
+
if (!v) return { ok: false, reason: "Pick a date value" };
|
|
3240
|
+
if (v.kind === "withinDays") {
|
|
3241
|
+
const maxDays = field.maxDays ?? 3650;
|
|
3242
|
+
if (v.days == null) return { ok: false, reason: "Enter a number of days" };
|
|
3243
|
+
if (!Number.isInteger(v.days)) return { ok: false, reason: "Enter a whole number" };
|
|
3244
|
+
if (v.days < 1) return { ok: false, reason: "Must be \u2265 1" };
|
|
3245
|
+
if (v.days > maxDays) return { ok: false, reason: "Must be \u2264 " + maxDays };
|
|
3246
|
+
return { ok: true };
|
|
3247
|
+
}
|
|
3248
|
+
if (!v.from || !v.to) return { ok: false, reason: "Pick both dates" };
|
|
3249
|
+
if (v.from > v.to) return { ok: false, reason: "The start date must be on or before the end date" };
|
|
3250
|
+
return { ok: true };
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
function isEmptyCandidate(field, candidate) {
|
|
3255
|
+
switch (field.type) {
|
|
3256
|
+
case "text":
|
|
3257
|
+
case "select":
|
|
3258
|
+
return false;
|
|
3259
|
+
case "multiSelect": {
|
|
3260
|
+
const v = candidate;
|
|
3261
|
+
return !v || !Array.isArray(v.values) || v.values.length === 0;
|
|
3262
|
+
}
|
|
3263
|
+
case "range": {
|
|
3264
|
+
const v = candidate;
|
|
3265
|
+
return !v || v.min == null && v.max == null;
|
|
3266
|
+
}
|
|
3267
|
+
case "date": {
|
|
3268
|
+
const v = candidate;
|
|
3269
|
+
if (!v) return true;
|
|
3270
|
+
return v.kind === "withinDays" ? v.days == null : !(v.from && v.to);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
function assertValidDateField(field) {
|
|
3275
|
+
if (process.env.NODE_ENV === "production") return;
|
|
3276
|
+
const defaultDays = field.defaultDays ?? 30;
|
|
3277
|
+
const maxDays = field.maxDays ?? 3650;
|
|
3278
|
+
if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays > 3650) {
|
|
3279
|
+
throw new Error(
|
|
3280
|
+
'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to 3650."
|
|
3281
|
+
);
|
|
3282
|
+
}
|
|
3283
|
+
if (!Number.isInteger(defaultDays) || defaultDays < 1 || defaultDays > maxDays) {
|
|
3284
|
+
throw new Error(
|
|
3285
|
+
'FilterBar: date field "' + field.key + '" has an invalid defaultDays (' + field.defaultDays + ") \u2014 must be a whole number from 1 to maxDays (" + maxDays + ")."
|
|
3286
|
+
);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3197
3289
|
function chipLabel(field, value) {
|
|
3198
3290
|
switch (field.type) {
|
|
3199
3291
|
case "text":
|
|
@@ -3246,8 +3338,7 @@ function MultiSelectPanel({
|
|
|
3246
3338
|
field,
|
|
3247
3339
|
value,
|
|
3248
3340
|
anchorRef,
|
|
3249
|
-
|
|
3250
|
-
onRemove,
|
|
3341
|
+
onCommit,
|
|
3251
3342
|
onClose
|
|
3252
3343
|
}) {
|
|
3253
3344
|
const allowedOps = operatorsFor(field);
|
|
@@ -3256,9 +3347,17 @@ function MultiSelectPanel({
|
|
|
3256
3347
|
const selected = value?.values ?? [];
|
|
3257
3348
|
const selectedRef = React24.useRef(selected);
|
|
3258
3349
|
selectedRef.current = selected;
|
|
3350
|
+
const operatorRef = React24.useRef(operator);
|
|
3351
|
+
operatorRef.current = operator;
|
|
3352
|
+
const mountedRef = React24.useRef(true);
|
|
3353
|
+
React24.useEffect(() => {
|
|
3354
|
+
mountedRef.current = true;
|
|
3355
|
+
return () => {
|
|
3356
|
+
mountedRef.current = false;
|
|
3357
|
+
};
|
|
3358
|
+
}, []);
|
|
3259
3359
|
const commit = (nextValues, nextOperator = operator) => {
|
|
3260
|
-
|
|
3261
|
-
else onChange({ operator: nextOperator, values: nextValues });
|
|
3360
|
+
onCommit({ operator: nextOperator, values: nextValues });
|
|
3262
3361
|
};
|
|
3263
3362
|
const setOperator = (op) => {
|
|
3264
3363
|
setDraftOperator(op);
|
|
@@ -3329,13 +3428,15 @@ function MultiSelectPanel({
|
|
|
3329
3428
|
setPasteLoading(true);
|
|
3330
3429
|
setPasteError(null);
|
|
3331
3430
|
field.resolve(tokens).then((result) => {
|
|
3431
|
+
if (!mountedRef.current) return;
|
|
3332
3432
|
setPasteLoading(false);
|
|
3333
3433
|
setUnmatchedTokens(result.unmatched);
|
|
3334
3434
|
const merged = new Map(selectedRef.current.map((s) => [s.value, s]));
|
|
3335
3435
|
for (const m of result.matched) merged.set(m.value, { value: m.value, label: m.label });
|
|
3336
|
-
commit(Array.from(merged.values()));
|
|
3436
|
+
commit(Array.from(merged.values()), operatorRef.current);
|
|
3337
3437
|
setPasteText("");
|
|
3338
3438
|
}).catch((err) => {
|
|
3439
|
+
if (!mountedRef.current) return;
|
|
3339
3440
|
setPasteLoading(false);
|
|
3340
3441
|
setPasteError(err instanceof Error ? err.message : "Could not resolve the pasted list.");
|
|
3341
3442
|
});
|
|
@@ -3471,22 +3572,21 @@ function RangePanel({
|
|
|
3471
3572
|
field,
|
|
3472
3573
|
value,
|
|
3473
3574
|
anchorRef,
|
|
3474
|
-
|
|
3475
|
-
onRemove,
|
|
3575
|
+
onCommit,
|
|
3476
3576
|
onClose
|
|
3477
3577
|
}) {
|
|
3478
3578
|
const [draftMin, setDraftMin] = React24.useState(value?.min);
|
|
3479
3579
|
const [draftMax, setDraftMax] = React24.useState(value?.max);
|
|
3480
|
-
const
|
|
3580
|
+
const [minTextValid, setMinTextValid] = React24.useState(true);
|
|
3581
|
+
const [maxTextValid, setMaxTextValid] = React24.useState(true);
|
|
3582
|
+
const isEmpty = draftMin == null && draftMax == null;
|
|
3583
|
+
const validation = isEmpty ? { ok: true } : validateFilterValue(field, { min: draftMin, max: draftMax });
|
|
3584
|
+
const comboError = !isEmpty && !validation.ok ? validation.reason : void 0;
|
|
3585
|
+
const doneDisabled = !minTextValid || !maxTextValid || !!comboError;
|
|
3481
3586
|
const apply = (nextMin, nextMax) => {
|
|
3482
3587
|
setDraftMin(nextMin);
|
|
3483
3588
|
setDraftMax(nextMax);
|
|
3484
|
-
|
|
3485
|
-
onRemove();
|
|
3486
|
-
return;
|
|
3487
|
-
}
|
|
3488
|
-
if (nextMin != null && nextMax != null && nextMin > nextMax) return;
|
|
3489
|
-
onChange({ min: nextMin, max: nextMax });
|
|
3589
|
+
onCommit(nextMin == null && nextMax == null ? void 0 : { min: nextMin, max: nextMax });
|
|
3490
3590
|
};
|
|
3491
3591
|
return /* @__PURE__ */ jsx45(
|
|
3492
3592
|
Popover,
|
|
@@ -3498,7 +3598,7 @@ function RangePanel({
|
|
|
3498
3598
|
offset: 6,
|
|
3499
3599
|
width: PANEL_WIDTH.range,
|
|
3500
3600
|
label: field.label,
|
|
3501
|
-
footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: onClose, disabled:
|
|
3601
|
+
footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: onClose, disabled: doneDisabled, children: "Done" }),
|
|
3502
3602
|
children: /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
|
|
3503
3603
|
/* @__PURE__ */ jsx45(
|
|
3504
3604
|
NumberInput,
|
|
@@ -3508,8 +3608,10 @@ function RangePanel({
|
|
|
3508
3608
|
min: field.min,
|
|
3509
3609
|
max: field.max,
|
|
3510
3610
|
step: field.step,
|
|
3611
|
+
integer: field.integer,
|
|
3511
3612
|
placeholder: field.minPlaceholder,
|
|
3512
|
-
onChange: (e) => apply(e.target.value, draftMax)
|
|
3613
|
+
onChange: (e) => apply(e.target.value, draftMax),
|
|
3614
|
+
onValidityChange: setMinTextValid
|
|
3513
3615
|
}
|
|
3514
3616
|
),
|
|
3515
3617
|
/* @__PURE__ */ jsx45(
|
|
@@ -3520,9 +3622,11 @@ function RangePanel({
|
|
|
3520
3622
|
min: field.min,
|
|
3521
3623
|
max: field.max,
|
|
3522
3624
|
step: field.step,
|
|
3523
|
-
|
|
3625
|
+
integer: field.integer,
|
|
3626
|
+
error: comboError,
|
|
3524
3627
|
placeholder: field.maxPlaceholder,
|
|
3525
|
-
onChange: (e) => apply(draftMin, e.target.value)
|
|
3628
|
+
onChange: (e) => apply(draftMin, e.target.value),
|
|
3629
|
+
onValidityChange: setMaxTextValid
|
|
3526
3630
|
}
|
|
3527
3631
|
)
|
|
3528
3632
|
] })
|
|
@@ -3533,8 +3637,7 @@ function DatePanel({
|
|
|
3533
3637
|
field,
|
|
3534
3638
|
value,
|
|
3535
3639
|
anchorRef,
|
|
3536
|
-
|
|
3537
|
-
onRemove,
|
|
3640
|
+
onCommit,
|
|
3538
3641
|
onClose
|
|
3539
3642
|
}) {
|
|
3540
3643
|
const defaultDays = field.defaultDays ?? 30;
|
|
@@ -3546,42 +3649,23 @@ function DatePanel({
|
|
|
3546
3649
|
const [between, setBetween] = React24.useState(
|
|
3547
3650
|
value?.kind === "between" ? { from: value.from, to: value.to } : { from: "", to: "" }
|
|
3548
3651
|
);
|
|
3549
|
-
const committedRef = React24.useRef(!!value);
|
|
3550
3652
|
const applyWithinDays = (days) => {
|
|
3551
3653
|
setDaysDraft(days);
|
|
3552
|
-
|
|
3553
|
-
committedRef.current = true;
|
|
3554
|
-
onChange({ kind: "withinDays", days });
|
|
3555
|
-
} else if (committedRef.current) {
|
|
3556
|
-
committedRef.current = false;
|
|
3557
|
-
onRemove();
|
|
3558
|
-
}
|
|
3654
|
+
onCommit(days == null ? void 0 : { kind: "withinDays", days });
|
|
3559
3655
|
};
|
|
3560
3656
|
const applyBetween = (from, to) => {
|
|
3561
3657
|
setBetween({ from, to });
|
|
3562
|
-
|
|
3563
|
-
committedRef.current = true;
|
|
3564
|
-
onChange({ kind: "between", from, to });
|
|
3565
|
-
} else if (committedRef.current) {
|
|
3566
|
-
committedRef.current = false;
|
|
3567
|
-
onRemove();
|
|
3568
|
-
}
|
|
3658
|
+
onCommit(from && to ? { kind: "between", from, to } : void 0);
|
|
3569
3659
|
};
|
|
3570
3660
|
const switchKind = (next) => {
|
|
3661
|
+
if (next === kind) return;
|
|
3571
3662
|
setKind(next);
|
|
3572
|
-
if (next === "between") {
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
committedRef.current = false;
|
|
3576
|
-
onRemove();
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3663
|
+
if (next === "between") setBetween({ from: "", to: "" });
|
|
3664
|
+
else setDaysDraft(defaultDays);
|
|
3665
|
+
onCommit(void 0);
|
|
3579
3666
|
};
|
|
3580
3667
|
const confirm = () => {
|
|
3581
|
-
if (kind === "withinDays" && daysDraft != null) {
|
|
3582
|
-
committedRef.current = true;
|
|
3583
|
-
onChange({ kind: "withinDays", days: daysDraft });
|
|
3584
|
-
}
|
|
3668
|
+
if (kind === "withinDays" && daysDraft != null) onCommit({ kind: "withinDays", days: daysDraft });
|
|
3585
3669
|
onClose();
|
|
3586
3670
|
};
|
|
3587
3671
|
return /* @__PURE__ */ jsx45(
|
|
@@ -3608,7 +3692,7 @@ function DatePanel({
|
|
|
3608
3692
|
onChange: (k) => switchKind(k)
|
|
3609
3693
|
}
|
|
3610
3694
|
),
|
|
3611
|
-
kind === "withinDays" ? /* @__PURE__ */ jsx45(NumberInput, { label: "Days", min: 1, max: maxDays, step: 1, value: daysDraft ?? "", onChange: (e) => applyWithinDays(e.target.value) }) : /* @__PURE__ */ jsx45(
|
|
3695
|
+
kind === "withinDays" ? /* @__PURE__ */ jsx45(NumberInput, { label: "Days", min: 1, max: maxDays, step: 1, integer: true, value: daysDraft ?? "", onChange: (e) => applyWithinDays(e.target.value) }) : /* @__PURE__ */ jsx45(
|
|
3612
3696
|
DatePicker,
|
|
3613
3697
|
{
|
|
3614
3698
|
range: true,
|
|
@@ -3627,6 +3711,21 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3627
3711
|
const addAnchorRef = React24.useRef(null);
|
|
3628
3712
|
const textInputRef = React24.useRef(null);
|
|
3629
3713
|
const optionsRef = React24.useRef(null);
|
|
3714
|
+
if (process.env.NODE_ENV !== "production") {
|
|
3715
|
+
for (const f of fields) if (f.type === "date") assertValidDateField(f);
|
|
3716
|
+
}
|
|
3717
|
+
const commit = (field, candidate) => {
|
|
3718
|
+
if (isEmptyCandidate(field, candidate)) {
|
|
3719
|
+
removeField(field);
|
|
3720
|
+
return { ok: true };
|
|
3721
|
+
}
|
|
3722
|
+
const result = validateFilterValue(field, candidate);
|
|
3723
|
+
if (result.ok) onFilterChange(field.key, candidate);
|
|
3724
|
+
return result;
|
|
3725
|
+
};
|
|
3726
|
+
const removeField = (field) => {
|
|
3727
|
+
if (isFieldSet(field, filters[field.key])) onFilterRemove(field.key);
|
|
3728
|
+
};
|
|
3630
3729
|
const editingField = panel && panel.kind === "edit" ? fields.find((f) => f.key === panel.key) : void 0;
|
|
3631
3730
|
const startEditing = (key) => setPanel({ kind: "edit", key });
|
|
3632
3731
|
const closeEditing = () => setPanel(null);
|
|
@@ -3658,7 +3757,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3658
3757
|
value: value ?? "",
|
|
3659
3758
|
placeholder: field.placeholder || field.label,
|
|
3660
3759
|
"aria-label": field.label,
|
|
3661
|
-
onChange: (e) =>
|
|
3760
|
+
onChange: (e) => commit(field, e.target.value),
|
|
3662
3761
|
onKeyDown: (e) => {
|
|
3663
3762
|
if (e.key === "Enter") setPanel(null);
|
|
3664
3763
|
else if (e.key === "Escape") {
|
|
@@ -3677,7 +3776,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3677
3776
|
"aria-label": "Remove " + field.label,
|
|
3678
3777
|
onClick: (e) => {
|
|
3679
3778
|
e.stopPropagation();
|
|
3680
|
-
|
|
3779
|
+
removeField(field);
|
|
3681
3780
|
setPanel(null);
|
|
3682
3781
|
},
|
|
3683
3782
|
children: /* @__PURE__ */ jsx45("i", { className: "ph ph-x", "aria-hidden": "true" })
|
|
@@ -3690,7 +3789,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3690
3789
|
const chipRef = editing ? editAnchorRef : void 0;
|
|
3691
3790
|
const toggleOrClose = () => editing ? closeEditing() : startEditing(field.key);
|
|
3692
3791
|
const remove = () => {
|
|
3693
|
-
|
|
3792
|
+
removeField(field);
|
|
3694
3793
|
closeEditing();
|
|
3695
3794
|
};
|
|
3696
3795
|
const chipText = field.label + (set ? ": " + chipLabel(field, value) : "");
|
|
@@ -3720,7 +3819,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3720
3819
|
"aria-selected": on,
|
|
3721
3820
|
className: ["fd-opt", on ? "is-selected" : ""].filter(Boolean).join(" "),
|
|
3722
3821
|
onClick: () => {
|
|
3723
|
-
|
|
3822
|
+
commit(field, o.value);
|
|
3724
3823
|
setPanel(null);
|
|
3725
3824
|
},
|
|
3726
3825
|
children: /* @__PURE__ */ jsx45("span", { className: "fd-opt-label", children: o.label })
|
|
@@ -3741,8 +3840,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3741
3840
|
field,
|
|
3742
3841
|
value,
|
|
3743
3842
|
anchorRef: editAnchorRef,
|
|
3744
|
-
|
|
3745
|
-
onRemove: () => onFilterRemove(field.key),
|
|
3843
|
+
onCommit: (v) => commit(field, v),
|
|
3746
3844
|
onClose: closeEditing
|
|
3747
3845
|
}
|
|
3748
3846
|
) : null
|
|
@@ -3757,8 +3855,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3757
3855
|
field,
|
|
3758
3856
|
value,
|
|
3759
3857
|
anchorRef: editAnchorRef,
|
|
3760
|
-
|
|
3761
|
-
onRemove: () => onFilterRemove(field.key),
|
|
3858
|
+
onCommit: (v) => commit(field, v),
|
|
3762
3859
|
onClose: closeEditing
|
|
3763
3860
|
}
|
|
3764
3861
|
) : null
|
|
@@ -3773,8 +3870,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3773
3870
|
field,
|
|
3774
3871
|
value,
|
|
3775
3872
|
anchorRef: editAnchorRef,
|
|
3776
|
-
|
|
3777
|
-
onRemove: () => onFilterRemove(field.key),
|
|
3873
|
+
onCommit: (v) => commit(field, v),
|
|
3778
3874
|
onClose: closeEditing
|
|
3779
3875
|
}
|
|
3780
3876
|
) : null
|