@flytedan/flytebot-design-system 0.31.0 → 0.32.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 +4 -4
- package/dist/index.cjs +219 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -20
- package/dist/index.d.ts +57 -20
- package/dist/index.js +218 -116
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- 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
|
|
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);
|
|
2801
2846
|
React21.useEffect(() => {
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
const bump = (
|
|
2813
|
-
const
|
|
2814
|
-
|
|
2815
|
-
let next = cur + dir * amount;
|
|
2816
|
-
if (integer) next = Math.round(next);
|
|
2817
|
-
next = clamp(next);
|
|
2818
|
-
setInvalidMessage(null);
|
|
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,11 +2964,15 @@ 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
|
] })
|
|
@@ -3201,16 +3257,31 @@ function isFieldSet(field, value) {
|
|
|
3201
3257
|
}
|
|
3202
3258
|
}
|
|
3203
3259
|
}
|
|
3260
|
+
var MAX_WITHIN_DAYS = 3650;
|
|
3261
|
+
function effectiveMaxDays(field) {
|
|
3262
|
+
return Math.min(field.maxDays ?? MAX_WITHIN_DAYS, MAX_WITHIN_DAYS);
|
|
3263
|
+
}
|
|
3264
|
+
function isIsoDate(v) {
|
|
3265
|
+
if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(v)) return false;
|
|
3266
|
+
const d = /* @__PURE__ */ new Date(v + "T00:00:00Z");
|
|
3267
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === v;
|
|
3268
|
+
}
|
|
3269
|
+
var isOption = (o) => !!o && typeof o.value === "string" && typeof o.label === "string";
|
|
3204
3270
|
function validateFilterValue(field, value) {
|
|
3205
3271
|
switch (field.type) {
|
|
3206
3272
|
case "text":
|
|
3273
|
+
return typeof value === "string" ? { ok: true } : { ok: false, reason: "Enter text" };
|
|
3207
3274
|
case "select":
|
|
3208
|
-
return { ok: true };
|
|
3275
|
+
return field.options.some((o) => o.value === value) ? { ok: true } : { ok: false, reason: "Pick one of the options" };
|
|
3209
3276
|
case "multiSelect": {
|
|
3210
3277
|
const v = value;
|
|
3211
3278
|
if (!v || !Array.isArray(v.values) || v.values.length === 0) {
|
|
3212
3279
|
return { ok: false, reason: "Select at least one value" };
|
|
3213
3280
|
}
|
|
3281
|
+
if (!v.values.every(isOption)) return { ok: false, reason: "Each value needs a value and a label" };
|
|
3282
|
+
if (field.maxValues != null && v.values.length > field.maxValues) {
|
|
3283
|
+
return { ok: false, reason: "Select at most " + field.maxValues + " values" };
|
|
3284
|
+
}
|
|
3214
3285
|
if (!operatorsFor(field).includes(v.operator)) {
|
|
3215
3286
|
return { ok: false, reason: "That operator isn't allowed for " + field.label };
|
|
3216
3287
|
}
|
|
@@ -3225,6 +3296,7 @@ function validateFilterValue(field, value) {
|
|
|
3225
3296
|
const hi = field.max ?? Infinity;
|
|
3226
3297
|
for (const n of [v.min, v.max]) {
|
|
3227
3298
|
if (n == null) continue;
|
|
3299
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return { ok: false, reason: "Enter a number" };
|
|
3228
3300
|
if (field.integer && !Number.isInteger(n)) return { ok: false, reason: "Enter a whole number" };
|
|
3229
3301
|
if (n < lo) return { ok: false, reason: "Must be \u2265 " + lo };
|
|
3230
3302
|
if (n > hi) return { ok: false, reason: "Must be \u2264 " + hi };
|
|
@@ -3238,46 +3310,36 @@ function validateFilterValue(field, value) {
|
|
|
3238
3310
|
const v = value;
|
|
3239
3311
|
if (!v) return { ok: false, reason: "Pick a date value" };
|
|
3240
3312
|
if (v.kind === "withinDays") {
|
|
3241
|
-
const maxDays = field
|
|
3313
|
+
const maxDays = effectiveMaxDays(field);
|
|
3242
3314
|
if (v.days == null) return { ok: false, reason: "Enter a number of days" };
|
|
3243
3315
|
if (!Number.isInteger(v.days)) return { ok: false, reason: "Enter a whole number" };
|
|
3244
3316
|
if (v.days < 1) return { ok: false, reason: "Must be \u2265 1" };
|
|
3245
3317
|
if (v.days > maxDays) return { ok: false, reason: "Must be \u2264 " + maxDays };
|
|
3246
3318
|
return { ok: true };
|
|
3247
3319
|
}
|
|
3320
|
+
if (v.kind !== "between") return { ok: false, reason: "Pick a date value" };
|
|
3248
3321
|
if (!v.from || !v.to) return { ok: false, reason: "Pick both dates" };
|
|
3322
|
+
if (!isIsoDate(v.from) || !isIsoDate(v.to)) return { ok: false, reason: "Dates must be YYYY-MM-DD" };
|
|
3249
3323
|
if (v.from > v.to) return { ok: false, reason: "The start date must be on or before the end date" };
|
|
3250
3324
|
return { ok: true };
|
|
3251
3325
|
}
|
|
3252
3326
|
}
|
|
3253
3327
|
}
|
|
3254
3328
|
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
|
-
}
|
|
3329
|
+
return field.type !== "text" && field.type !== "select" && !isFieldSet(field, candidate);
|
|
3273
3330
|
}
|
|
3274
|
-
function
|
|
3275
|
-
if (
|
|
3331
|
+
function assertValidFieldDefinition(field) {
|
|
3332
|
+
if (field.type === "multiSelect" && field.maxValues != null && (!Number.isInteger(field.maxValues) || field.maxValues < 1)) {
|
|
3333
|
+
throw new Error(
|
|
3334
|
+
'FilterBar: multiSelect field "' + field.key + '" has an invalid maxValues (' + field.maxValues + ") \u2014 must be a whole number of at least 1."
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
if (field.type !== "date") return;
|
|
3276
3338
|
const defaultDays = field.defaultDays ?? 30;
|
|
3277
|
-
const maxDays = field.maxDays ??
|
|
3278
|
-
if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays >
|
|
3339
|
+
const maxDays = field.maxDays ?? MAX_WITHIN_DAYS;
|
|
3340
|
+
if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays > MAX_WITHIN_DAYS) {
|
|
3279
3341
|
throw new Error(
|
|
3280
|
-
'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to
|
|
3342
|
+
'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to " + MAX_WITHIN_DAYS + "."
|
|
3281
3343
|
);
|
|
3282
3344
|
}
|
|
3283
3345
|
if (!Number.isInteger(defaultDays) || defaultDays < 1 || defaultDays > maxDays) {
|
|
@@ -3361,7 +3423,7 @@ function MultiSelectPanel({
|
|
|
3361
3423
|
};
|
|
3362
3424
|
const setOperator = (op) => {
|
|
3363
3425
|
setDraftOperator(op);
|
|
3364
|
-
if (
|
|
3426
|
+
if (selectedRef.current.length > 0) commit(selectedRef.current, op);
|
|
3365
3427
|
};
|
|
3366
3428
|
const [mode2, setMode] = React24.useState("search");
|
|
3367
3429
|
const [query, setQuery] = React24.useState("");
|
|
@@ -3401,9 +3463,12 @@ function MultiSelectPanel({
|
|
|
3401
3463
|
const showError = !!field.loadOptions && asyncState.error;
|
|
3402
3464
|
const hasListbox = !showLoading && !showError && options.length > 0;
|
|
3403
3465
|
const isSelected = (o) => selected.some((s) => s.value === o.value);
|
|
3466
|
+
const atLimit = field.maxValues != null && selected.length >= field.maxValues;
|
|
3404
3467
|
const toggle = (o) => {
|
|
3405
|
-
const
|
|
3406
|
-
|
|
3468
|
+
const current2 = selectedRef.current;
|
|
3469
|
+
const on = current2.some((s) => s.value === o.value);
|
|
3470
|
+
if (!on && field.maxValues != null && current2.length >= field.maxValues) return;
|
|
3471
|
+
commit(on ? current2.filter((s) => s.value !== o.value) : [...current2, o]);
|
|
3407
3472
|
};
|
|
3408
3473
|
const nav = useListboxNav({ count: options.length, onSelect: (i) => toggle(options[i]) });
|
|
3409
3474
|
useListboxScrollIntoView(nav.active, listRef, hasListbox);
|
|
@@ -3411,8 +3476,7 @@ function MultiSelectPanel({
|
|
|
3411
3476
|
nav.setActive(0);
|
|
3412
3477
|
}, [query, options.length]);
|
|
3413
3478
|
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" : "";
|
|
3479
|
+
const liveMessage = showLoading ? "Loading\u2026" : showError ? "Couldn't load options." : options.length === 0 ? "No matches" : atLimit ? "Limit reached: at most " + field.maxValues + " values" : "";
|
|
3416
3480
|
const [pasteText, setPasteText] = React24.useState("");
|
|
3417
3481
|
const [pasteLoading, setPasteLoading] = React24.useState(false);
|
|
3418
3482
|
const [unmatchedTokens, setUnmatchedTokens] = React24.useState([]);
|
|
@@ -3433,9 +3497,13 @@ function MultiSelectPanel({
|
|
|
3433
3497
|
setUnmatchedTokens(result.unmatched);
|
|
3434
3498
|
const merged = new Map(selectedRef.current.map((s) => [s.value, s]));
|
|
3435
3499
|
for (const m of result.matched) merged.set(m.value, { value: m.value, label: m.label });
|
|
3500
|
+
if (field.maxValues != null && merged.size > field.maxValues) {
|
|
3501
|
+
setPasteError("This filter allows at most " + field.maxValues + " values; adding this list would make " + merged.size + ".");
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
3436
3504
|
commit(Array.from(merged.values()), operatorRef.current);
|
|
3437
3505
|
setPasteText("");
|
|
3438
|
-
}
|
|
3506
|
+
}, (err) => {
|
|
3439
3507
|
if (!mountedRef.current) return;
|
|
3440
3508
|
setPasteLoading(false);
|
|
3441
3509
|
setPasteError(err instanceof Error ? err.message : "Could not resolve the pasted list.");
|
|
@@ -3514,14 +3582,14 @@ function MultiSelectPanel({
|
|
|
3514
3582
|
selected.length,
|
|
3515
3583
|
" matched"
|
|
3516
3584
|
] }),
|
|
3517
|
-
/* @__PURE__ */ jsx45("span", { className: "fd-row fd-wrap fd-gap-xs", children: selected.map((v) => /* @__PURE__ */ jsx45(Tag, { onRemove: () => commit(
|
|
3585
|
+
/* @__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
3586
|
] }) : null
|
|
3519
3587
|
] }) : /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
|
|
3520
3588
|
/* @__PURE__ */ jsxs39(
|
|
3521
3589
|
"span",
|
|
3522
3590
|
{
|
|
3523
3591
|
className: "fd-row fd-gap-xs" + (liveMessage && !showError ? " fd-muted" : ""),
|
|
3524
|
-
role:
|
|
3592
|
+
role: "status",
|
|
3525
3593
|
children: [
|
|
3526
3594
|
showLoading ? /* @__PURE__ */ jsx45("span", { className: "fd-spinner", "aria-hidden": "true" }) : null,
|
|
3527
3595
|
liveMessage ? /* @__PURE__ */ jsx45("span", { className: "fd-body-sm", children: liveMessage }) : null
|
|
@@ -3531,6 +3599,7 @@ function MultiSelectPanel({
|
|
|
3531
3599
|
showError ? /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: () => setRetryTick((n) => n + 1), children: "Retry" }) : null,
|
|
3532
3600
|
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
3601
|
const on = isSelected(o);
|
|
3602
|
+
const blocked = !on && atLimit;
|
|
3534
3603
|
return /* @__PURE__ */ jsxs39(
|
|
3535
3604
|
"div",
|
|
3536
3605
|
{
|
|
@@ -3539,6 +3608,7 @@ function MultiSelectPanel({
|
|
|
3539
3608
|
role: "option",
|
|
3540
3609
|
tabIndex: -1,
|
|
3541
3610
|
"aria-selected": on,
|
|
3611
|
+
"aria-disabled": blocked ? "true" : void 0,
|
|
3542
3612
|
className: ["fd-opt", i === nav.active ? "is-active" : "", on ? "is-selected" : ""].filter(Boolean).join(" "),
|
|
3543
3613
|
onMouseEnter: () => nav.setActive(i),
|
|
3544
3614
|
onClick: () => toggle(o),
|
|
@@ -3575,18 +3645,21 @@ function RangePanel({
|
|
|
3575
3645
|
onCommit,
|
|
3576
3646
|
onClose
|
|
3577
3647
|
}) {
|
|
3578
|
-
const [
|
|
3579
|
-
const
|
|
3648
|
+
const [draft, setDraft] = React24.useState({ min: value?.min, max: value?.max });
|
|
3649
|
+
const draftRef = React24.useRef(draft);
|
|
3580
3650
|
const [minTextValid, setMinTextValid] = React24.useState(true);
|
|
3581
3651
|
const [maxTextValid, setMaxTextValid] = React24.useState(true);
|
|
3652
|
+
const draftMin = draft.min;
|
|
3653
|
+
const draftMax = draft.max;
|
|
3582
3654
|
const isEmpty = draftMin == null && draftMax == null;
|
|
3583
3655
|
const validation = isEmpty ? { ok: true } : validateFilterValue(field, { min: draftMin, max: draftMax });
|
|
3584
3656
|
const comboError = !isEmpty && !validation.ok ? validation.reason : void 0;
|
|
3585
3657
|
const doneDisabled = !minTextValid || !maxTextValid || !!comboError;
|
|
3586
|
-
const apply = (
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3658
|
+
const apply = (patch) => {
|
|
3659
|
+
const next = { ...draftRef.current, ...patch };
|
|
3660
|
+
draftRef.current = next;
|
|
3661
|
+
setDraft(next);
|
|
3662
|
+
onCommit(next.min == null && next.max == null ? void 0 : next);
|
|
3590
3663
|
};
|
|
3591
3664
|
return /* @__PURE__ */ jsx45(
|
|
3592
3665
|
Popover,
|
|
@@ -3610,7 +3683,7 @@ function RangePanel({
|
|
|
3610
3683
|
step: field.step,
|
|
3611
3684
|
integer: field.integer,
|
|
3612
3685
|
placeholder: field.minPlaceholder,
|
|
3613
|
-
onChange: (e) => apply(e.target.value
|
|
3686
|
+
onChange: (e) => apply({ min: e.target.value }),
|
|
3614
3687
|
onValidityChange: setMinTextValid
|
|
3615
3688
|
}
|
|
3616
3689
|
),
|
|
@@ -3625,7 +3698,7 @@ function RangePanel({
|
|
|
3625
3698
|
integer: field.integer,
|
|
3626
3699
|
error: comboError,
|
|
3627
3700
|
placeholder: field.maxPlaceholder,
|
|
3628
|
-
onChange: (e) => apply(
|
|
3701
|
+
onChange: (e) => apply({ max: e.target.value }),
|
|
3629
3702
|
onValidityChange: setMaxTextValid
|
|
3630
3703
|
}
|
|
3631
3704
|
)
|
|
@@ -3641,7 +3714,8 @@ function DatePanel({
|
|
|
3641
3714
|
onClose
|
|
3642
3715
|
}) {
|
|
3643
3716
|
const defaultDays = field.defaultDays ?? 30;
|
|
3644
|
-
const maxDays = field
|
|
3717
|
+
const maxDays = effectiveMaxDays(field);
|
|
3718
|
+
const [daysTextValid, setDaysTextValid] = React24.useState(true);
|
|
3645
3719
|
const [kind, setKind] = React24.useState(value?.kind ?? "withinDays");
|
|
3646
3720
|
const [daysDraft, setDaysDraft] = React24.useState(
|
|
3647
3721
|
value?.kind === "withinDays" ? value.days : defaultDays
|
|
@@ -3662,9 +3736,12 @@ function DatePanel({
|
|
|
3662
3736
|
setKind(next);
|
|
3663
3737
|
if (next === "between") setBetween({ from: "", to: "" });
|
|
3664
3738
|
else setDaysDraft(defaultDays);
|
|
3739
|
+
setDaysTextValid(true);
|
|
3665
3740
|
onCommit(void 0);
|
|
3666
3741
|
};
|
|
3742
|
+
const doneDisabled = kind === "withinDays" && !daysTextValid;
|
|
3667
3743
|
const confirm = () => {
|
|
3744
|
+
if (doneDisabled) return;
|
|
3668
3745
|
if (kind === "withinDays" && daysDraft != null) onCommit({ kind: "withinDays", days: daysDraft });
|
|
3669
3746
|
onClose();
|
|
3670
3747
|
};
|
|
@@ -3678,7 +3755,7 @@ function DatePanel({
|
|
|
3678
3755
|
offset: 6,
|
|
3679
3756
|
width: PANEL_WIDTH.date,
|
|
3680
3757
|
label: field.label,
|
|
3681
|
-
footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: confirm, children: "Done" }),
|
|
3758
|
+
footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: confirm, disabled: doneDisabled, children: "Done" }),
|
|
3682
3759
|
children: /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
|
|
3683
3760
|
/* @__PURE__ */ jsx45(
|
|
3684
3761
|
SegmentedControl,
|
|
@@ -3692,7 +3769,19 @@ function DatePanel({
|
|
|
3692
3769
|
onChange: (k) => switchKind(k)
|
|
3693
3770
|
}
|
|
3694
3771
|
),
|
|
3695
|
-
kind === "withinDays" ? /* @__PURE__ */ jsx45(
|
|
3772
|
+
kind === "withinDays" ? /* @__PURE__ */ jsx45(
|
|
3773
|
+
NumberInput,
|
|
3774
|
+
{
|
|
3775
|
+
label: "Days",
|
|
3776
|
+
min: 1,
|
|
3777
|
+
max: maxDays,
|
|
3778
|
+
step: 1,
|
|
3779
|
+
integer: true,
|
|
3780
|
+
value: daysDraft ?? "",
|
|
3781
|
+
onChange: (e) => applyWithinDays(e.target.value),
|
|
3782
|
+
onValidityChange: setDaysTextValid
|
|
3783
|
+
}
|
|
3784
|
+
) : /* @__PURE__ */ jsx45(
|
|
3696
3785
|
DatePicker,
|
|
3697
3786
|
{
|
|
3698
3787
|
range: true,
|
|
@@ -3712,19 +3801,31 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3712
3801
|
const textInputRef = React24.useRef(null);
|
|
3713
3802
|
const optionsRef = React24.useRef(null);
|
|
3714
3803
|
if (process.env.NODE_ENV !== "production") {
|
|
3715
|
-
for (const f of fields)
|
|
3804
|
+
for (const f of fields) assertValidFieldDefinition(f);
|
|
3716
3805
|
}
|
|
3806
|
+
const latest = React24.useRef({ fields, filters, onFilterChange, onFilterRemove });
|
|
3807
|
+
latest.current = { fields, filters, onFilterChange, onFilterRemove };
|
|
3717
3808
|
const commit = (field, candidate) => {
|
|
3718
|
-
|
|
3719
|
-
|
|
3809
|
+
const current2 = latest.current.fields.find((f) => f.key === field.key);
|
|
3810
|
+
if (!current2) return { ok: false, reason: "This filter is no longer available" };
|
|
3811
|
+
if (isEmptyCandidate(current2, candidate)) {
|
|
3812
|
+
removeField(current2);
|
|
3720
3813
|
return { ok: true };
|
|
3721
3814
|
}
|
|
3722
|
-
const result = validateFilterValue(
|
|
3723
|
-
if (result.ok)
|
|
3815
|
+
const result = validateFilterValue(current2, candidate);
|
|
3816
|
+
if (result.ok) {
|
|
3817
|
+
latest.current.filters = { ...latest.current.filters, [current2.key]: candidate };
|
|
3818
|
+
latest.current.onFilterChange(current2.key, candidate);
|
|
3819
|
+
}
|
|
3724
3820
|
return result;
|
|
3725
3821
|
};
|
|
3726
3822
|
const removeField = (field) => {
|
|
3727
|
-
|
|
3823
|
+
const { filters: now, onFilterRemove: remove } = latest.current;
|
|
3824
|
+
if (!isFieldSet(field, now[field.key])) return;
|
|
3825
|
+
const next = { ...now };
|
|
3826
|
+
delete next[field.key];
|
|
3827
|
+
latest.current.filters = next;
|
|
3828
|
+
remove(field.key);
|
|
3728
3829
|
};
|
|
3729
3830
|
const editingField = panel && panel.kind === "edit" ? fields.find((f) => f.key === panel.key) : void 0;
|
|
3730
3831
|
const startEditing = (key) => setPanel({ kind: "edit", key });
|
|
@@ -13718,6 +13819,7 @@ export {
|
|
|
13718
13819
|
useServerTable,
|
|
13719
13820
|
useSessionContext,
|
|
13720
13821
|
useStagedFiles,
|
|
13822
|
+
validateFilterValue,
|
|
13721
13823
|
visibleMessages
|
|
13722
13824
|
};
|
|
13723
13825
|
//# sourceMappingURL=index.js.map
|