@flytedan/flytebot-design-system 0.31.0 → 0.33.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/README.md +6 -4
- package/dist/index.cjs +288 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -23
- package/dist/index.d.ts +65 -23
- package/dist/index.js +287 -131
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/styles/components.css +8 -0
package/dist/index.js
CHANGED
|
@@ -2757,6 +2757,48 @@ function useTriggerLabelling(label, id) {
|
|
|
2757
2757
|
|
|
2758
2758
|
// src/components/forms/NumberInput.tsx
|
|
2759
2759
|
import { jsx as jsx42, jsxs as jsxs36 } from "react/jsx-runtime";
|
|
2760
|
+
var HOLD_DELAY_MS = 400;
|
|
2761
|
+
var HOLD_BIG_AFTER = 6;
|
|
2762
|
+
var HOLD_FAST_AFTER = 12;
|
|
2763
|
+
var holdInterval = (repeats) => repeats > HOLD_FAST_AFTER ? 40 : repeats > HOLD_BIG_AFTER ? 70 : 120;
|
|
2764
|
+
function decimalPlaces(n) {
|
|
2765
|
+
if (!Number.isFinite(n)) return 0;
|
|
2766
|
+
const [mantissa, exp] = String(n).toLowerCase().split("e");
|
|
2767
|
+
const frac = (mantissa.split(".")[1] || "").length;
|
|
2768
|
+
return Math.max(0, frac - (exp ? Number(exp) : 0));
|
|
2769
|
+
}
|
|
2770
|
+
function parseNumberText(text) {
|
|
2771
|
+
const s = text.replace(/,/g, "").trim();
|
|
2772
|
+
if (s === "") return null;
|
|
2773
|
+
if (!/^-?(\d+\.?\d*|\.\d+)$/.test(s)) return NaN;
|
|
2774
|
+
return Number(s);
|
|
2775
|
+
}
|
|
2776
|
+
function allowedRange(min, max, integer) {
|
|
2777
|
+
return integer ? { lo: Math.ceil(min), hi: Math.floor(max) } : { lo: min, hi: max };
|
|
2778
|
+
}
|
|
2779
|
+
function stepNumber({ from, direction, amount, step, min, max, integer }) {
|
|
2780
|
+
const { lo, hi } = allowedRange(min, max, integer);
|
|
2781
|
+
if (lo > hi) return null;
|
|
2782
|
+
const base = from ?? 0;
|
|
2783
|
+
const anchor = Number.isFinite(lo) ? lo : 0;
|
|
2784
|
+
const unit = step > 0 ? step : 1;
|
|
2785
|
+
const travel = amount > 0 ? amount : unit;
|
|
2786
|
+
const magnitude = Math.max(Math.abs(base), Math.abs(anchor), travel, unit, 1);
|
|
2787
|
+
const maxPlaces = Math.max(0, Math.floor(Math.log10(Number.MAX_SAFE_INTEGER / (4 * magnitude))));
|
|
2788
|
+
const places = Math.min(maxPlaces, Math.max(decimalPlaces(base), decimalPlaces(anchor), decimalPlaces(travel), decimalPlaces(unit)));
|
|
2789
|
+
const scale = 10 ** places;
|
|
2790
|
+
const b = Math.round(base * scale);
|
|
2791
|
+
const a = Math.round(anchor * scale);
|
|
2792
|
+
const s = Math.round(unit * scale);
|
|
2793
|
+
const t = Math.round(travel * scale);
|
|
2794
|
+
const target = b + direction * t;
|
|
2795
|
+
let k = direction > 0 ? Math.floor((target - a) / s) : Math.ceil((target - a) / s);
|
|
2796
|
+
if (direction > 0 && a + k * s <= b) k = Math.floor((b - a) / s) + 1;
|
|
2797
|
+
if (direction < 0 && a + k * s >= b) k = Math.ceil((b - a) / s) - 1;
|
|
2798
|
+
let next = (a + k * s) / scale;
|
|
2799
|
+
if (integer && !Number.isInteger(next)) next = direction > 0 ? Math.ceil(next) : Math.floor(next);
|
|
2800
|
+
return Math.min(hi, Math.max(lo, next));
|
|
2801
|
+
}
|
|
2760
2802
|
function NumberInput({
|
|
2761
2803
|
label,
|
|
2762
2804
|
help,
|
|
@@ -2782,70 +2824,78 @@ function NumberInput({
|
|
|
2782
2824
|
}) {
|
|
2783
2825
|
const fieldId = useFieldId(id);
|
|
2784
2826
|
const errorId = fieldId + "-error";
|
|
2785
|
-
const
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
if (integer && !Number.isInteger(n)) return "Enter a whole number";
|
|
2793
|
-
if (n < min) return "Must be \u2265 " + min;
|
|
2794
|
-
if (n > max) return "Must be \u2264 " + max;
|
|
2795
|
-
return null;
|
|
2827
|
+
const { lo, hi } = allowedRange(min, max, integer);
|
|
2828
|
+
const big = bigStep ?? Number((step * 10).toFixed(decimalPlaces(step)));
|
|
2829
|
+
const toText = (v) => v == null || v === "" ? "" : String(v);
|
|
2830
|
+
const toNumber = (v) => {
|
|
2831
|
+
if (v == null || v === "") return null;
|
|
2832
|
+
const n = typeof v === "number" ? v : parseNumberText(v);
|
|
2833
|
+
return n == null || Number.isNaN(n) ? null : n;
|
|
2796
2834
|
};
|
|
2797
|
-
const [text, setText] = React21.useState(
|
|
2835
|
+
const [text, setText] = React21.useState(() => toText(value));
|
|
2798
2836
|
const [editing, setEditing] = React21.useState(false);
|
|
2799
|
-
const
|
|
2800
|
-
const
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2837
|
+
const parsed = parseNumberText(text);
|
|
2838
|
+
const invalidMessage = parsed == null ? null : Number.isNaN(parsed) ? "Enter a number" : integer && !Number.isInteger(parsed) ? "Enter a whole number" : parsed < lo ? "Must be \u2265 " + lo : parsed > hi ? "Must be \u2264 " + hi : null;
|
|
2839
|
+
const valid = invalidMessage == null;
|
|
2840
|
+
const committedRef = React21.useRef(toNumber(value));
|
|
2841
|
+
const onChangeRef = React21.useRef(onChange);
|
|
2842
|
+
onChangeRef.current = onChange;
|
|
2843
|
+
const onValidityChangeRef = React21.useRef(onValidityChange);
|
|
2844
|
+
onValidityChangeRef.current = onValidityChange;
|
|
2845
|
+
const focusedRef = React21.useRef(false);
|
|
2807
2846
|
React21.useEffect(() => {
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2847
|
+
committedRef.current = toNumber(value);
|
|
2848
|
+
if (!focusedRef.current) setText(toText(value));
|
|
2849
|
+
}, [value]);
|
|
2850
|
+
React21.useLayoutEffect(() => {
|
|
2851
|
+
onValidityChangeRef.current?.(valid);
|
|
2852
|
+
}, [valid]);
|
|
2853
|
+
const fire = (n) => {
|
|
2854
|
+
committedRef.current = n ?? null;
|
|
2855
|
+
onChangeRef.current?.({ target: { value: n } });
|
|
2856
|
+
};
|
|
2857
|
+
const bump = (direction, amount) => {
|
|
2858
|
+
const next = stepNumber({ from: committedRef.current, direction, amount, step, min, max, integer });
|
|
2859
|
+
if (next == null) return false;
|
|
2819
2860
|
setText(String(next));
|
|
2861
|
+
if (next === committedRef.current) return false;
|
|
2820
2862
|
fire(next);
|
|
2863
|
+
return true;
|
|
2821
2864
|
};
|
|
2822
|
-
const
|
|
2823
|
-
|
|
2824
|
-
|
|
2865
|
+
const bumpRef = React21.useRef(bump);
|
|
2866
|
+
bumpRef.current = bump;
|
|
2867
|
+
const holdTimer = React21.useRef(null);
|
|
2868
|
+
const release = () => {
|
|
2869
|
+
if (holdTimer.current) clearTimeout(holdTimer.current);
|
|
2870
|
+
holdTimer.current = null;
|
|
2871
|
+
};
|
|
2872
|
+
const hold = (direction) => {
|
|
2873
|
+
release();
|
|
2874
|
+
bump(direction, step);
|
|
2875
|
+
let repeats = 0;
|
|
2825
2876
|
const tick = () => {
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2877
|
+
repeats++;
|
|
2878
|
+
if (!bumpRef.current(direction, repeats > HOLD_BIG_AFTER ? big : step)) {
|
|
2879
|
+
release();
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
holdTimer.current = setTimeout(tick, holdInterval(repeats));
|
|
2829
2883
|
};
|
|
2830
|
-
|
|
2831
|
-
};
|
|
2832
|
-
const release = () => {
|
|
2833
|
-
if (timer.current) clearTimeout(timer.current);
|
|
2884
|
+
holdTimer.current = setTimeout(tick, HOLD_DELAY_MS);
|
|
2834
2885
|
};
|
|
2835
2886
|
React21.useEffect(() => () => {
|
|
2836
|
-
if (
|
|
2887
|
+
if (holdTimer.current) clearTimeout(holdTimer.current);
|
|
2837
2888
|
}, []);
|
|
2838
|
-
const
|
|
2839
|
-
|
|
2840
|
-
return n == null ? "" : format ? n.toLocaleString() : String(n);
|
|
2841
|
-
})();
|
|
2889
|
+
const current2 = committedRef.current;
|
|
2890
|
+
const shown = editing || !valid ? text : parsed == null ? "" : format ? parsed.toLocaleString() : String(parsed);
|
|
2842
2891
|
const shownError = error || invalidMessage || void 0;
|
|
2843
2892
|
const box = ["fd-input", "fd-input-num", shownError ? "fd-input-error" : "", disabled ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
|
|
2844
2893
|
const fieldName = label || ariaLabel;
|
|
2845
2894
|
const stepperName = (verb) => fieldName ? verb + " " + fieldName : verb;
|
|
2895
|
+
const noRange = lo > hi;
|
|
2846
2896
|
return /* @__PURE__ */ jsxs36("div", { className: ["fd-field", className].filter(Boolean).join(" "), style, children: [
|
|
2847
2897
|
label ? /* @__PURE__ */ jsx42(FieldLabel, { htmlFor: fieldId, required, children: label }) : null,
|
|
2848
|
-
/* @__PURE__ */ jsxs36("div", { className: box,
|
|
2898
|
+
/* @__PURE__ */ jsxs36("div", { className: box, children: [
|
|
2849
2899
|
prefix ? /* @__PURE__ */ jsx42("span", { className: "fd-input-affix", children: prefix }) : null,
|
|
2850
2900
|
/* @__PURE__ */ jsx42(
|
|
2851
2901
|
"input",
|
|
@@ -2858,52 +2908,54 @@ function NumberInput({
|
|
|
2858
2908
|
value: shown,
|
|
2859
2909
|
"aria-invalid": shownError ? "true" : void 0,
|
|
2860
2910
|
"aria-describedby": shownError ? errorId : void 0,
|
|
2861
|
-
onFocus: () =>
|
|
2911
|
+
onFocus: () => {
|
|
2912
|
+
focusedRef.current = true;
|
|
2913
|
+
setEditing(true);
|
|
2914
|
+
},
|
|
2862
2915
|
onBlur: () => {
|
|
2916
|
+
focusedRef.current = false;
|
|
2863
2917
|
setEditing(false);
|
|
2864
|
-
if (text.trim() === "") fire(void 0);
|
|
2918
|
+
if (text.trim() === "" && committedRef.current != null) fire(void 0);
|
|
2865
2919
|
},
|
|
2866
2920
|
onChange: (e) => {
|
|
2867
2921
|
const raw = e.target.value;
|
|
2868
2922
|
setText(raw);
|
|
2869
|
-
|
|
2870
|
-
setInvalidMessage(null);
|
|
2871
|
-
fire(void 0);
|
|
2872
|
-
return;
|
|
2873
|
-
}
|
|
2874
|
-
const n = num(raw);
|
|
2923
|
+
const n = parseNumberText(raw);
|
|
2875
2924
|
if (n == null) {
|
|
2876
|
-
|
|
2925
|
+
if (committedRef.current != null) fire(void 0);
|
|
2877
2926
|
return;
|
|
2878
2927
|
}
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
if (!message) fire(n);
|
|
2928
|
+
if (Number.isNaN(n) || integer && !Number.isInteger(n) || n < lo || n > hi) return;
|
|
2929
|
+
fire(n);
|
|
2882
2930
|
},
|
|
2883
2931
|
onKeyDown: (e) => {
|
|
2884
2932
|
if (e.key === "ArrowUp") {
|
|
2885
2933
|
e.preventDefault();
|
|
2886
|
-
bump(1, e.shiftKey ?
|
|
2934
|
+
bump(1, e.shiftKey ? big : step);
|
|
2887
2935
|
} else if (e.key === "ArrowDown") {
|
|
2888
2936
|
e.preventDefault();
|
|
2889
|
-
bump(-1, e.shiftKey ?
|
|
2937
|
+
bump(-1, e.shiftKey ? big : step);
|
|
2890
2938
|
}
|
|
2891
2939
|
}
|
|
2892
2940
|
}
|
|
2893
2941
|
),
|
|
2894
2942
|
suffix ? /* @__PURE__ */ jsx42("span", { className: "fd-input-affix", children: suffix }) : null,
|
|
2895
|
-
/* @__PURE__ */ jsxs36("span", { className: "fd-
|
|
2943
|
+
/* @__PURE__ */ jsxs36("span", { className: "fd-input-steppers", children: [
|
|
2896
2944
|
/* @__PURE__ */ jsx42(
|
|
2897
2945
|
"button",
|
|
2898
2946
|
{
|
|
2899
2947
|
type: "button",
|
|
2900
2948
|
className: "fd-stepbtn",
|
|
2901
2949
|
"aria-label": stepperName("Decrease"),
|
|
2902
|
-
disabled: disabled ||
|
|
2950
|
+
disabled: disabled || noRange || current2 != null && current2 <= lo,
|
|
2903
2951
|
onPointerDown: () => hold(-1),
|
|
2904
2952
|
onPointerUp: release,
|
|
2905
2953
|
onPointerLeave: release,
|
|
2906
|
-
|
|
2954
|
+
onPointerCancel: release,
|
|
2955
|
+
onClick: (e) => {
|
|
2956
|
+
if (e.detail === 0) bump(-1, step);
|
|
2957
|
+
},
|
|
2958
|
+
children: /* @__PURE__ */ jsx42("i", { className: "ph ph-minus", "aria-hidden": "true" })
|
|
2907
2959
|
}
|
|
2908
2960
|
),
|
|
2909
2961
|
/* @__PURE__ */ jsx42(
|
|
@@ -2912,16 +2964,21 @@ function NumberInput({
|
|
|
2912
2964
|
type: "button",
|
|
2913
2965
|
className: "fd-stepbtn",
|
|
2914
2966
|
"aria-label": stepperName("Increase"),
|
|
2915
|
-
disabled: disabled ||
|
|
2967
|
+
disabled: disabled || noRange || current2 != null && current2 >= hi,
|
|
2916
2968
|
onPointerDown: () => hold(1),
|
|
2917
2969
|
onPointerUp: release,
|
|
2918
2970
|
onPointerLeave: release,
|
|
2919
|
-
|
|
2971
|
+
onPointerCancel: release,
|
|
2972
|
+
onClick: (e) => {
|
|
2973
|
+
if (e.detail === 0) bump(1, step);
|
|
2974
|
+
},
|
|
2975
|
+
children: /* @__PURE__ */ jsx42("i", { className: "ph ph-plus", "aria-hidden": "true" })
|
|
2920
2976
|
}
|
|
2921
2977
|
)
|
|
2922
2978
|
] })
|
|
2923
2979
|
] }),
|
|
2924
|
-
|
|
2980
|
+
/* @__PURE__ */ jsx42("span", { id: errorId, className: "fd-sr", role: "status", children: shownError ?? "" }),
|
|
2981
|
+
shownError ? /* @__PURE__ */ jsxs36("span", { className: "fd-field-error", "aria-hidden": "true", children: [
|
|
2925
2982
|
/* @__PURE__ */ jsx42("i", { className: "ph ph-warning-circle", "aria-hidden": "true" }),
|
|
2926
2983
|
shownError
|
|
2927
2984
|
] }) : help ? /* @__PURE__ */ jsx42("span", { className: "fd-field-help", children: help }) : null
|
|
@@ -3201,16 +3258,31 @@ function isFieldSet(field, value) {
|
|
|
3201
3258
|
}
|
|
3202
3259
|
}
|
|
3203
3260
|
}
|
|
3261
|
+
var MAX_WITHIN_DAYS = 3650;
|
|
3262
|
+
function effectiveMaxDays(field) {
|
|
3263
|
+
return Math.min(field.maxDays ?? MAX_WITHIN_DAYS, MAX_WITHIN_DAYS);
|
|
3264
|
+
}
|
|
3265
|
+
function isIsoDate(v) {
|
|
3266
|
+
if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(v)) return false;
|
|
3267
|
+
const d = /* @__PURE__ */ new Date(v + "T00:00:00Z");
|
|
3268
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === v;
|
|
3269
|
+
}
|
|
3270
|
+
var isOption = (o) => !!o && typeof o.value === "string" && typeof o.label === "string";
|
|
3204
3271
|
function validateFilterValue(field, value) {
|
|
3205
3272
|
switch (field.type) {
|
|
3206
3273
|
case "text":
|
|
3274
|
+
return typeof value === "string" ? { ok: true } : { ok: false, reason: "Enter text" };
|
|
3207
3275
|
case "select":
|
|
3208
|
-
return { ok: true };
|
|
3276
|
+
return field.options.some((o) => o.value === value) ? { ok: true } : { ok: false, reason: "Pick one of the options" };
|
|
3209
3277
|
case "multiSelect": {
|
|
3210
3278
|
const v = value;
|
|
3211
3279
|
if (!v || !Array.isArray(v.values) || v.values.length === 0) {
|
|
3212
3280
|
return { ok: false, reason: "Select at least one value" };
|
|
3213
3281
|
}
|
|
3282
|
+
if (!v.values.every(isOption)) return { ok: false, reason: "Each value needs a value and a label" };
|
|
3283
|
+
if (field.maxValues != null && v.values.length > field.maxValues) {
|
|
3284
|
+
return { ok: false, reason: "Select at most " + field.maxValues + " values" };
|
|
3285
|
+
}
|
|
3214
3286
|
if (!operatorsFor(field).includes(v.operator)) {
|
|
3215
3287
|
return { ok: false, reason: "That operator isn't allowed for " + field.label };
|
|
3216
3288
|
}
|
|
@@ -3225,6 +3297,7 @@ function validateFilterValue(field, value) {
|
|
|
3225
3297
|
const hi = field.max ?? Infinity;
|
|
3226
3298
|
for (const n of [v.min, v.max]) {
|
|
3227
3299
|
if (n == null) continue;
|
|
3300
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return { ok: false, reason: "Enter a number" };
|
|
3228
3301
|
if (field.integer && !Number.isInteger(n)) return { ok: false, reason: "Enter a whole number" };
|
|
3229
3302
|
if (n < lo) return { ok: false, reason: "Must be \u2265 " + lo };
|
|
3230
3303
|
if (n > hi) return { ok: false, reason: "Must be \u2264 " + hi };
|
|
@@ -3238,46 +3311,36 @@ function validateFilterValue(field, value) {
|
|
|
3238
3311
|
const v = value;
|
|
3239
3312
|
if (!v) return { ok: false, reason: "Pick a date value" };
|
|
3240
3313
|
if (v.kind === "withinDays") {
|
|
3241
|
-
const maxDays = field
|
|
3314
|
+
const maxDays = effectiveMaxDays(field);
|
|
3242
3315
|
if (v.days == null) return { ok: false, reason: "Enter a number of days" };
|
|
3243
3316
|
if (!Number.isInteger(v.days)) return { ok: false, reason: "Enter a whole number" };
|
|
3244
3317
|
if (v.days < 1) return { ok: false, reason: "Must be \u2265 1" };
|
|
3245
3318
|
if (v.days > maxDays) return { ok: false, reason: "Must be \u2264 " + maxDays };
|
|
3246
3319
|
return { ok: true };
|
|
3247
3320
|
}
|
|
3321
|
+
if (v.kind !== "between") return { ok: false, reason: "Pick a date value" };
|
|
3248
3322
|
if (!v.from || !v.to) return { ok: false, reason: "Pick both dates" };
|
|
3323
|
+
if (!isIsoDate(v.from) || !isIsoDate(v.to)) return { ok: false, reason: "Dates must be YYYY-MM-DD" };
|
|
3249
3324
|
if (v.from > v.to) return { ok: false, reason: "The start date must be on or before the end date" };
|
|
3250
3325
|
return { ok: true };
|
|
3251
3326
|
}
|
|
3252
3327
|
}
|
|
3253
3328
|
}
|
|
3254
3329
|
function isEmptyCandidate(field, candidate) {
|
|
3255
|
-
|
|
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
|
-
}
|
|
3330
|
+
return field.type !== "text" && field.type !== "select" && !isFieldSet(field, candidate);
|
|
3273
3331
|
}
|
|
3274
|
-
function
|
|
3275
|
-
if (
|
|
3332
|
+
function assertValidFieldDefinition(field) {
|
|
3333
|
+
if (field.type === "multiSelect" && field.maxValues != null && (!Number.isInteger(field.maxValues) || field.maxValues < 1)) {
|
|
3334
|
+
throw new Error(
|
|
3335
|
+
'FilterBar: multiSelect field "' + field.key + '" has an invalid maxValues (' + field.maxValues + ") \u2014 must be a whole number of at least 1."
|
|
3336
|
+
);
|
|
3337
|
+
}
|
|
3338
|
+
if (field.type !== "date") return;
|
|
3276
3339
|
const defaultDays = field.defaultDays ?? 30;
|
|
3277
|
-
const maxDays = field.maxDays ??
|
|
3278
|
-
if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays >
|
|
3340
|
+
const maxDays = field.maxDays ?? MAX_WITHIN_DAYS;
|
|
3341
|
+
if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays > MAX_WITHIN_DAYS) {
|
|
3279
3342
|
throw new Error(
|
|
3280
|
-
'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to
|
|
3343
|
+
'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to " + MAX_WITHIN_DAYS + "."
|
|
3281
3344
|
);
|
|
3282
3345
|
}
|
|
3283
3346
|
if (!Number.isInteger(defaultDays) || defaultDays < 1 || defaultDays > maxDays) {
|
|
@@ -3356,12 +3419,10 @@ function MultiSelectPanel({
|
|
|
3356
3419
|
mountedRef.current = false;
|
|
3357
3420
|
};
|
|
3358
3421
|
}, []);
|
|
3359
|
-
const commit = (nextValues, nextOperator = operator) => {
|
|
3360
|
-
onCommit({ operator: nextOperator, values: nextValues });
|
|
3361
|
-
};
|
|
3422
|
+
const commit = (nextValues, nextOperator = operator) => onCommit({ operator: nextOperator, values: nextValues });
|
|
3362
3423
|
const setOperator = (op) => {
|
|
3363
3424
|
setDraftOperator(op);
|
|
3364
|
-
if (
|
|
3425
|
+
if (selectedRef.current.length > 0) commit(selectedRef.current, op);
|
|
3365
3426
|
};
|
|
3366
3427
|
const [mode2, setMode] = React24.useState("search");
|
|
3367
3428
|
const [query, setQuery] = React24.useState("");
|
|
@@ -3401,9 +3462,12 @@ function MultiSelectPanel({
|
|
|
3401
3462
|
const showError = !!field.loadOptions && asyncState.error;
|
|
3402
3463
|
const hasListbox = !showLoading && !showError && options.length > 0;
|
|
3403
3464
|
const isSelected = (o) => selected.some((s) => s.value === o.value);
|
|
3465
|
+
const atLimit = field.maxValues != null && selected.length >= field.maxValues;
|
|
3404
3466
|
const toggle = (o) => {
|
|
3405
|
-
const
|
|
3406
|
-
|
|
3467
|
+
const current2 = selectedRef.current;
|
|
3468
|
+
const on = current2.some((s) => s.value === o.value);
|
|
3469
|
+
if (!on && field.maxValues != null && current2.length >= field.maxValues) return;
|
|
3470
|
+
commit(on ? current2.filter((s) => s.value !== o.value) : [...current2, o]);
|
|
3407
3471
|
};
|
|
3408
3472
|
const nav = useListboxNav({ count: options.length, onSelect: (i) => toggle(options[i]) });
|
|
3409
3473
|
useListboxScrollIntoView(nav.active, listRef, hasListbox);
|
|
@@ -3411,8 +3475,7 @@ function MultiSelectPanel({
|
|
|
3411
3475
|
nav.setActive(0);
|
|
3412
3476
|
}, [query, options.length]);
|
|
3413
3477
|
const activeId = hasListbox && options[nav.active] ? listId + "-opt-" + nav.active : void 0;
|
|
3414
|
-
const
|
|
3415
|
-
const liveMessage = showLoading ? "Loading\u2026" : showError ? "Couldn't load options." : options.length === 0 ? "No matches" : "";
|
|
3478
|
+
const liveMessage = showLoading ? "Loading\u2026" : showError ? "Couldn't load options." : options.length === 0 ? "No matches" : atLimit ? "Limit reached: at most " + field.maxValues + " values" : "";
|
|
3416
3479
|
const [pasteText, setPasteText] = React24.useState("");
|
|
3417
3480
|
const [pasteLoading, setPasteLoading] = React24.useState(false);
|
|
3418
3481
|
const [unmatchedTokens, setUnmatchedTokens] = React24.useState([]);
|
|
@@ -3433,9 +3496,17 @@ function MultiSelectPanel({
|
|
|
3433
3496
|
setUnmatchedTokens(result.unmatched);
|
|
3434
3497
|
const merged = new Map(selectedRef.current.map((s) => [s.value, s]));
|
|
3435
3498
|
for (const m of result.matched) merged.set(m.value, { value: m.value, label: m.label });
|
|
3436
|
-
|
|
3499
|
+
if (field.maxValues != null && merged.size > field.maxValues) {
|
|
3500
|
+
setPasteError("This filter allows at most " + field.maxValues + " values; adding this list would make " + merged.size + ".");
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
const applied = commit(Array.from(merged.values()), operatorRef.current);
|
|
3504
|
+
if (!applied.ok) {
|
|
3505
|
+
setPasteError(applied.reason);
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3437
3508
|
setPasteText("");
|
|
3438
|
-
}
|
|
3509
|
+
}, (err) => {
|
|
3439
3510
|
if (!mountedRef.current) return;
|
|
3440
3511
|
setPasteLoading(false);
|
|
3441
3512
|
setPasteError(err instanceof Error ? err.message : "Could not resolve the pasted list.");
|
|
@@ -3514,14 +3585,14 @@ function MultiSelectPanel({
|
|
|
3514
3585
|
selected.length,
|
|
3515
3586
|
" matched"
|
|
3516
3587
|
] }),
|
|
3517
|
-
/* @__PURE__ */ jsx45("span", { className: "fd-row fd-wrap fd-gap-xs", children: selected.map((v) => /* @__PURE__ */ jsx45(Tag, { onRemove: () => commit(
|
|
3588
|
+
/* @__PURE__ */ jsx45("span", { className: "fd-row fd-wrap fd-gap-xs", children: selected.map((v) => /* @__PURE__ */ jsx45(Tag, { onRemove: () => commit(selectedRef.current.filter((s) => s.value !== v.value)), children: v.label }, v.value)) })
|
|
3518
3589
|
] }) : null
|
|
3519
3590
|
] }) : /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
|
|
3520
3591
|
/* @__PURE__ */ jsxs39(
|
|
3521
3592
|
"span",
|
|
3522
3593
|
{
|
|
3523
3594
|
className: "fd-row fd-gap-xs" + (liveMessage && !showError ? " fd-muted" : ""),
|
|
3524
|
-
role:
|
|
3595
|
+
role: "status",
|
|
3525
3596
|
children: [
|
|
3526
3597
|
showLoading ? /* @__PURE__ */ jsx45("span", { className: "fd-spinner", "aria-hidden": "true" }) : null,
|
|
3527
3598
|
liveMessage ? /* @__PURE__ */ jsx45("span", { className: "fd-body-sm", children: liveMessage }) : null
|
|
@@ -3531,6 +3602,7 @@ function MultiSelectPanel({
|
|
|
3531
3602
|
showError ? /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: () => setRetryTick((n) => n + 1), children: "Retry" }) : null,
|
|
3532
3603
|
hasListbox ? /* @__PURE__ */ jsx45("span", { id: listId, role: "listbox", "aria-label": field.label, "aria-multiselectable": "true", ref: listRef, className: "fd-pop-list", children: options.map((o, i) => {
|
|
3533
3604
|
const on = isSelected(o);
|
|
3605
|
+
const blocked = !on && atLimit;
|
|
3534
3606
|
return /* @__PURE__ */ jsxs39(
|
|
3535
3607
|
"div",
|
|
3536
3608
|
{
|
|
@@ -3539,6 +3611,7 @@ function MultiSelectPanel({
|
|
|
3539
3611
|
role: "option",
|
|
3540
3612
|
tabIndex: -1,
|
|
3541
3613
|
"aria-selected": on,
|
|
3614
|
+
"aria-disabled": blocked ? "true" : void 0,
|
|
3542
3615
|
className: ["fd-opt", i === nav.active ? "is-active" : "", on ? "is-selected" : ""].filter(Boolean).join(" "),
|
|
3543
3616
|
onMouseEnter: () => nav.setActive(i),
|
|
3544
3617
|
onClick: () => toggle(o),
|
|
@@ -3568,6 +3641,34 @@ function MultiSelectPanel({
|
|
|
3568
3641
|
}
|
|
3569
3642
|
);
|
|
3570
3643
|
}
|
|
3644
|
+
function useTextValidity(onFlip) {
|
|
3645
|
+
const invalidRef = React24.useRef(/* @__PURE__ */ new Set());
|
|
3646
|
+
const [, rerender] = React24.useReducer((n) => n + 1, 0);
|
|
3647
|
+
const onFlipRef = React24.useRef(onFlip);
|
|
3648
|
+
onFlipRef.current = onFlip;
|
|
3649
|
+
const record = (name, valid) => {
|
|
3650
|
+
const invalid = invalidRef.current;
|
|
3651
|
+
if (valid !== invalid.has(name)) return false;
|
|
3652
|
+
if (valid) invalid.delete(name);
|
|
3653
|
+
else invalid.add(name);
|
|
3654
|
+
rerender();
|
|
3655
|
+
return true;
|
|
3656
|
+
};
|
|
3657
|
+
return {
|
|
3658
|
+
allValid: () => invalidRef.current.size === 0,
|
|
3659
|
+
report: (name) => (valid) => {
|
|
3660
|
+
if (record(name, valid)) onFlipRef.current();
|
|
3661
|
+
},
|
|
3662
|
+
/** For an input that is about to unmount: it has nothing left to show. */
|
|
3663
|
+
forget: (name) => {
|
|
3664
|
+
record(name, true);
|
|
3665
|
+
}
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
function applicable(field, candidate, textValid) {
|
|
3669
|
+
if (!textValid || candidate === void 0) return void 0;
|
|
3670
|
+
return validateFilterValue(field, candidate).ok ? candidate : void 0;
|
|
3671
|
+
}
|
|
3571
3672
|
function RangePanel({
|
|
3572
3673
|
field,
|
|
3573
3674
|
value,
|
|
@@ -3575,18 +3676,24 @@ function RangePanel({
|
|
|
3575
3676
|
onCommit,
|
|
3576
3677
|
onClose
|
|
3577
3678
|
}) {
|
|
3578
|
-
const [
|
|
3579
|
-
const
|
|
3580
|
-
const
|
|
3581
|
-
|
|
3679
|
+
const [draft, setDraft] = React24.useState({ min: value?.min, max: value?.max });
|
|
3680
|
+
const draftRef = React24.useRef(draft);
|
|
3681
|
+
const publish = () => {
|
|
3682
|
+
const { min, max } = draftRef.current;
|
|
3683
|
+
onCommit(applicable(field, min == null && max == null ? void 0 : { min, max }, validity.allValid()));
|
|
3684
|
+
};
|
|
3685
|
+
const validity = useTextValidity(publish);
|
|
3686
|
+
const draftMin = draft.min;
|
|
3687
|
+
const draftMax = draft.max;
|
|
3582
3688
|
const isEmpty = draftMin == null && draftMax == null;
|
|
3583
3689
|
const validation = isEmpty ? { ok: true } : validateFilterValue(field, { min: draftMin, max: draftMax });
|
|
3584
3690
|
const comboError = !isEmpty && !validation.ok ? validation.reason : void 0;
|
|
3585
|
-
const doneDisabled = !
|
|
3586
|
-
const apply = (
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3691
|
+
const doneDisabled = !validity.allValid() || !!comboError;
|
|
3692
|
+
const apply = (patch) => {
|
|
3693
|
+
const next = { ...draftRef.current, ...patch };
|
|
3694
|
+
draftRef.current = next;
|
|
3695
|
+
setDraft(next);
|
|
3696
|
+
publish();
|
|
3590
3697
|
};
|
|
3591
3698
|
return /* @__PURE__ */ jsx45(
|
|
3592
3699
|
Popover,
|
|
@@ -3610,8 +3717,8 @@ function RangePanel({
|
|
|
3610
3717
|
step: field.step,
|
|
3611
3718
|
integer: field.integer,
|
|
3612
3719
|
placeholder: field.minPlaceholder,
|
|
3613
|
-
onChange: (e) => apply(e.target.value
|
|
3614
|
-
onValidityChange:
|
|
3720
|
+
onChange: (e) => apply({ min: e.target.value }),
|
|
3721
|
+
onValidityChange: validity.report("min")
|
|
3615
3722
|
}
|
|
3616
3723
|
),
|
|
3617
3724
|
/* @__PURE__ */ jsx45(
|
|
@@ -3625,8 +3732,8 @@ function RangePanel({
|
|
|
3625
3732
|
integer: field.integer,
|
|
3626
3733
|
error: comboError,
|
|
3627
3734
|
placeholder: field.maxPlaceholder,
|
|
3628
|
-
onChange: (e) => apply(
|
|
3629
|
-
onValidityChange:
|
|
3735
|
+
onChange: (e) => apply({ max: e.target.value }),
|
|
3736
|
+
onValidityChange: validity.report("max")
|
|
3630
3737
|
}
|
|
3631
3738
|
)
|
|
3632
3739
|
] })
|
|
@@ -3641,31 +3748,48 @@ function DatePanel({
|
|
|
3641
3748
|
onClose
|
|
3642
3749
|
}) {
|
|
3643
3750
|
const defaultDays = field.defaultDays ?? 30;
|
|
3644
|
-
const maxDays = field
|
|
3751
|
+
const maxDays = effectiveMaxDays(field);
|
|
3645
3752
|
const [kind, setKind] = React24.useState(value?.kind ?? "withinDays");
|
|
3646
3753
|
const [daysDraft, setDaysDraft] = React24.useState(
|
|
3647
3754
|
value?.kind === "withinDays" ? value.days : defaultDays
|
|
3648
3755
|
);
|
|
3756
|
+
const daysRef = React24.useRef(daysDraft);
|
|
3649
3757
|
const [between, setBetween] = React24.useState(
|
|
3650
3758
|
value?.kind === "between" ? { from: value.from, to: value.to } : { from: "", to: "" }
|
|
3651
3759
|
);
|
|
3760
|
+
const shownWithinDays = () => {
|
|
3761
|
+
const days = daysRef.current;
|
|
3762
|
+
return applicable(field, days == null ? void 0 : { kind: "withinDays", days }, validity.allValid());
|
|
3763
|
+
};
|
|
3764
|
+
const validity = useTextValidity(() => onCommit(shownWithinDays()));
|
|
3652
3765
|
const applyWithinDays = (days) => {
|
|
3766
|
+
daysRef.current = days;
|
|
3653
3767
|
setDaysDraft(days);
|
|
3654
|
-
onCommit(
|
|
3768
|
+
onCommit(shownWithinDays());
|
|
3655
3769
|
};
|
|
3656
3770
|
const applyBetween = (from, to) => {
|
|
3657
3771
|
setBetween({ from, to });
|
|
3658
|
-
onCommit(from && to ? { kind: "between", from, to } : void 0);
|
|
3772
|
+
onCommit(applicable(field, from && to ? { kind: "between", from, to } : void 0, true));
|
|
3659
3773
|
};
|
|
3660
3774
|
const switchKind = (next) => {
|
|
3661
3775
|
if (next === kind) return;
|
|
3662
3776
|
setKind(next);
|
|
3663
|
-
if (next === "between")
|
|
3664
|
-
|
|
3777
|
+
if (next === "between") {
|
|
3778
|
+
setBetween({ from: "", to: "" });
|
|
3779
|
+
validity.forget("days");
|
|
3780
|
+
} else {
|
|
3781
|
+
daysRef.current = defaultDays;
|
|
3782
|
+
setDaysDraft(defaultDays);
|
|
3783
|
+
}
|
|
3665
3784
|
onCommit(void 0);
|
|
3666
3785
|
};
|
|
3786
|
+
const doneDisabled = kind === "withinDays" && !validity.allValid();
|
|
3667
3787
|
const confirm = () => {
|
|
3668
|
-
if (
|
|
3788
|
+
if (doneDisabled) return;
|
|
3789
|
+
if (kind === "withinDays") {
|
|
3790
|
+
const shown = shownWithinDays();
|
|
3791
|
+
if (shown) onCommit(shown);
|
|
3792
|
+
}
|
|
3669
3793
|
onClose();
|
|
3670
3794
|
};
|
|
3671
3795
|
return /* @__PURE__ */ jsx45(
|
|
@@ -3678,7 +3802,7 @@ function DatePanel({
|
|
|
3678
3802
|
offset: 6,
|
|
3679
3803
|
width: PANEL_WIDTH.date,
|
|
3680
3804
|
label: field.label,
|
|
3681
|
-
footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: confirm, children: "Done" }),
|
|
3805
|
+
footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: confirm, disabled: doneDisabled, children: "Done" }),
|
|
3682
3806
|
children: /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
|
|
3683
3807
|
/* @__PURE__ */ jsx45(
|
|
3684
3808
|
SegmentedControl,
|
|
@@ -3692,7 +3816,19 @@ function DatePanel({
|
|
|
3692
3816
|
onChange: (k) => switchKind(k)
|
|
3693
3817
|
}
|
|
3694
3818
|
),
|
|
3695
|
-
kind === "withinDays" ? /* @__PURE__ */ jsx45(
|
|
3819
|
+
kind === "withinDays" ? /* @__PURE__ */ jsx45(
|
|
3820
|
+
NumberInput,
|
|
3821
|
+
{
|
|
3822
|
+
label: "Days",
|
|
3823
|
+
min: 1,
|
|
3824
|
+
max: maxDays,
|
|
3825
|
+
step: 1,
|
|
3826
|
+
integer: true,
|
|
3827
|
+
value: daysDraft ?? "",
|
|
3828
|
+
onChange: (e) => applyWithinDays(e.target.value),
|
|
3829
|
+
onValidityChange: validity.report("days")
|
|
3830
|
+
}
|
|
3831
|
+
) : /* @__PURE__ */ jsx45(
|
|
3696
3832
|
DatePicker,
|
|
3697
3833
|
{
|
|
3698
3834
|
range: true,
|
|
@@ -3712,19 +3848,38 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3712
3848
|
const textInputRef = React24.useRef(null);
|
|
3713
3849
|
const optionsRef = React24.useRef(null);
|
|
3714
3850
|
if (process.env.NODE_ENV !== "production") {
|
|
3715
|
-
for (const f of fields)
|
|
3851
|
+
for (const f of fields) assertValidFieldDefinition(f);
|
|
3716
3852
|
}
|
|
3853
|
+
const latest = React24.useRef({ fields, filters, onFilterChange, onFilterRemove });
|
|
3854
|
+
const filtersProp = React24.useRef(filters);
|
|
3855
|
+
if (filters !== filtersProp.current) {
|
|
3856
|
+
filtersProp.current = filters;
|
|
3857
|
+
latest.current.filters = filters;
|
|
3858
|
+
}
|
|
3859
|
+
latest.current.fields = fields;
|
|
3860
|
+
latest.current.onFilterChange = onFilterChange;
|
|
3861
|
+
latest.current.onFilterRemove = onFilterRemove;
|
|
3717
3862
|
const commit = (field, candidate) => {
|
|
3718
|
-
|
|
3719
|
-
|
|
3863
|
+
const current2 = latest.current.fields.find((f) => f.key === field.key);
|
|
3864
|
+
if (!current2) return { ok: false, reason: "This filter is no longer available" };
|
|
3865
|
+
if (isEmptyCandidate(current2, candidate)) {
|
|
3866
|
+
removeField(current2);
|
|
3720
3867
|
return { ok: true };
|
|
3721
3868
|
}
|
|
3722
|
-
const result = validateFilterValue(
|
|
3723
|
-
if (result.ok)
|
|
3869
|
+
const result = validateFilterValue(current2, candidate);
|
|
3870
|
+
if (result.ok) {
|
|
3871
|
+
latest.current.filters = { ...latest.current.filters, [current2.key]: candidate };
|
|
3872
|
+
latest.current.onFilterChange(current2.key, candidate);
|
|
3873
|
+
}
|
|
3724
3874
|
return result;
|
|
3725
3875
|
};
|
|
3726
3876
|
const removeField = (field) => {
|
|
3727
|
-
|
|
3877
|
+
const { filters: now, onFilterRemove: remove } = latest.current;
|
|
3878
|
+
if (!isFieldSet(field, now[field.key])) return;
|
|
3879
|
+
const next = { ...now };
|
|
3880
|
+
delete next[field.key];
|
|
3881
|
+
latest.current.filters = next;
|
|
3882
|
+
remove(field.key);
|
|
3728
3883
|
};
|
|
3729
3884
|
const editingField = panel && panel.kind === "edit" ? fields.find((f) => f.key === panel.key) : void 0;
|
|
3730
3885
|
const startEditing = (key) => setPanel({ kind: "edit", key });
|
|
@@ -13718,6 +13873,7 @@ export {
|
|
|
13718
13873
|
useServerTable,
|
|
13719
13874
|
useSessionContext,
|
|
13720
13875
|
useStagedFiles,
|
|
13876
|
+
validateFilterValue,
|
|
13721
13877
|
visibleMessages
|
|
13722
13878
|
};
|
|
13723
13879
|
//# sourceMappingURL=index.js.map
|