@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.cjs
CHANGED
|
@@ -233,6 +233,7 @@ __export(index_exports, {
|
|
|
233
233
|
useServerTable: () => useServerTable,
|
|
234
234
|
useSessionContext: () => useSessionContext,
|
|
235
235
|
useStagedFiles: () => useStagedFiles,
|
|
236
|
+
validateFilterValue: () => validateFilterValue,
|
|
236
237
|
visibleMessages: () => visibleMessages
|
|
237
238
|
});
|
|
238
239
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -2996,6 +2997,48 @@ function useTriggerLabelling(label, id) {
|
|
|
2996
2997
|
|
|
2997
2998
|
// src/components/forms/NumberInput.tsx
|
|
2998
2999
|
var import_jsx_runtime42 = require("react/jsx-runtime");
|
|
3000
|
+
var HOLD_DELAY_MS = 400;
|
|
3001
|
+
var HOLD_BIG_AFTER = 6;
|
|
3002
|
+
var HOLD_FAST_AFTER = 12;
|
|
3003
|
+
var holdInterval = (repeats) => repeats > HOLD_FAST_AFTER ? 40 : repeats > HOLD_BIG_AFTER ? 70 : 120;
|
|
3004
|
+
function decimalPlaces(n) {
|
|
3005
|
+
if (!Number.isFinite(n)) return 0;
|
|
3006
|
+
const [mantissa, exp] = String(n).toLowerCase().split("e");
|
|
3007
|
+
const frac = (mantissa.split(".")[1] || "").length;
|
|
3008
|
+
return Math.max(0, frac - (exp ? Number(exp) : 0));
|
|
3009
|
+
}
|
|
3010
|
+
function parseNumberText(text) {
|
|
3011
|
+
const s = text.replace(/,/g, "").trim();
|
|
3012
|
+
if (s === "") return null;
|
|
3013
|
+
if (!/^-?(\d+\.?\d*|\.\d+)$/.test(s)) return NaN;
|
|
3014
|
+
return Number(s);
|
|
3015
|
+
}
|
|
3016
|
+
function allowedRange(min, max, integer) {
|
|
3017
|
+
return integer ? { lo: Math.ceil(min), hi: Math.floor(max) } : { lo: min, hi: max };
|
|
3018
|
+
}
|
|
3019
|
+
function stepNumber({ from, direction, amount, step, min, max, integer }) {
|
|
3020
|
+
const { lo, hi } = allowedRange(min, max, integer);
|
|
3021
|
+
if (lo > hi) return null;
|
|
3022
|
+
const base = from ?? 0;
|
|
3023
|
+
const anchor = Number.isFinite(lo) ? lo : 0;
|
|
3024
|
+
const unit = step > 0 ? step : 1;
|
|
3025
|
+
const travel = amount > 0 ? amount : unit;
|
|
3026
|
+
const magnitude = Math.max(Math.abs(base), Math.abs(anchor), travel, unit, 1);
|
|
3027
|
+
const maxPlaces = Math.max(0, Math.floor(Math.log10(Number.MAX_SAFE_INTEGER / (4 * magnitude))));
|
|
3028
|
+
const places = Math.min(maxPlaces, Math.max(decimalPlaces(base), decimalPlaces(anchor), decimalPlaces(travel), decimalPlaces(unit)));
|
|
3029
|
+
const scale = 10 ** places;
|
|
3030
|
+
const b = Math.round(base * scale);
|
|
3031
|
+
const a = Math.round(anchor * scale);
|
|
3032
|
+
const s = Math.round(unit * scale);
|
|
3033
|
+
const t = Math.round(travel * scale);
|
|
3034
|
+
const target = b + direction * t;
|
|
3035
|
+
let k = direction > 0 ? Math.floor((target - a) / s) : Math.ceil((target - a) / s);
|
|
3036
|
+
if (direction > 0 && a + k * s <= b) k = Math.floor((b - a) / s) + 1;
|
|
3037
|
+
if (direction < 0 && a + k * s >= b) k = Math.ceil((b - a) / s) - 1;
|
|
3038
|
+
let next = (a + k * s) / scale;
|
|
3039
|
+
if (integer && !Number.isInteger(next)) next = direction > 0 ? Math.ceil(next) : Math.floor(next);
|
|
3040
|
+
return Math.min(hi, Math.max(lo, next));
|
|
3041
|
+
}
|
|
2999
3042
|
function NumberInput({
|
|
3000
3043
|
label,
|
|
3001
3044
|
help,
|
|
@@ -3021,70 +3064,78 @@ function NumberInput({
|
|
|
3021
3064
|
}) {
|
|
3022
3065
|
const fieldId = useFieldId(id);
|
|
3023
3066
|
const errorId = fieldId + "-error";
|
|
3024
|
-
const
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
if (integer && !Number.isInteger(n)) return "Enter a whole number";
|
|
3032
|
-
if (n < min) return "Must be \u2265 " + min;
|
|
3033
|
-
if (n > max) return "Must be \u2264 " + max;
|
|
3034
|
-
return null;
|
|
3067
|
+
const { lo, hi } = allowedRange(min, max, integer);
|
|
3068
|
+
const big = bigStep ?? Number((step * 10).toFixed(decimalPlaces(step)));
|
|
3069
|
+
const toText = (v) => v == null || v === "" ? "" : String(v);
|
|
3070
|
+
const toNumber = (v) => {
|
|
3071
|
+
if (v == null || v === "") return null;
|
|
3072
|
+
const n = typeof v === "number" ? v : parseNumberText(v);
|
|
3073
|
+
return n == null || Number.isNaN(n) ? null : n;
|
|
3035
3074
|
};
|
|
3036
|
-
const [text, setText] = React21.useState(
|
|
3075
|
+
const [text, setText] = React21.useState(() => toText(value));
|
|
3037
3076
|
const [editing, setEditing] = React21.useState(false);
|
|
3038
|
-
const
|
|
3039
|
-
const
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3077
|
+
const parsed = parseNumberText(text);
|
|
3078
|
+
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;
|
|
3079
|
+
const valid = invalidMessage == null;
|
|
3080
|
+
const committedRef = React21.useRef(toNumber(value));
|
|
3081
|
+
const onChangeRef = React21.useRef(onChange);
|
|
3082
|
+
onChangeRef.current = onChange;
|
|
3083
|
+
const onValidityChangeRef = React21.useRef(onValidityChange);
|
|
3084
|
+
onValidityChangeRef.current = onValidityChange;
|
|
3085
|
+
const focusedRef = React21.useRef(false);
|
|
3046
3086
|
React21.useEffect(() => {
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3087
|
+
committedRef.current = toNumber(value);
|
|
3088
|
+
if (!focusedRef.current) setText(toText(value));
|
|
3089
|
+
}, [value]);
|
|
3090
|
+
React21.useLayoutEffect(() => {
|
|
3091
|
+
onValidityChangeRef.current?.(valid);
|
|
3092
|
+
}, [valid]);
|
|
3093
|
+
const fire = (n) => {
|
|
3094
|
+
committedRef.current = n ?? null;
|
|
3095
|
+
onChangeRef.current?.({ target: { value: n } });
|
|
3096
|
+
};
|
|
3097
|
+
const bump = (direction, amount) => {
|
|
3098
|
+
const next = stepNumber({ from: committedRef.current, direction, amount, step, min, max, integer });
|
|
3099
|
+
if (next == null) return false;
|
|
3058
3100
|
setText(String(next));
|
|
3101
|
+
if (next === committedRef.current) return false;
|
|
3059
3102
|
fire(next);
|
|
3103
|
+
return true;
|
|
3060
3104
|
};
|
|
3061
|
-
const
|
|
3062
|
-
|
|
3063
|
-
|
|
3105
|
+
const bumpRef = React21.useRef(bump);
|
|
3106
|
+
bumpRef.current = bump;
|
|
3107
|
+
const holdTimer = React21.useRef(null);
|
|
3108
|
+
const release = () => {
|
|
3109
|
+
if (holdTimer.current) clearTimeout(holdTimer.current);
|
|
3110
|
+
holdTimer.current = null;
|
|
3111
|
+
};
|
|
3112
|
+
const hold = (direction) => {
|
|
3113
|
+
release();
|
|
3114
|
+
bump(direction, step);
|
|
3115
|
+
let repeats = 0;
|
|
3064
3116
|
const tick = () => {
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3117
|
+
repeats++;
|
|
3118
|
+
if (!bumpRef.current(direction, repeats > HOLD_BIG_AFTER ? big : step)) {
|
|
3119
|
+
release();
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
holdTimer.current = setTimeout(tick, holdInterval(repeats));
|
|
3068
3123
|
};
|
|
3069
|
-
|
|
3070
|
-
};
|
|
3071
|
-
const release = () => {
|
|
3072
|
-
if (timer.current) clearTimeout(timer.current);
|
|
3124
|
+
holdTimer.current = setTimeout(tick, HOLD_DELAY_MS);
|
|
3073
3125
|
};
|
|
3074
3126
|
React21.useEffect(() => () => {
|
|
3075
|
-
if (
|
|
3127
|
+
if (holdTimer.current) clearTimeout(holdTimer.current);
|
|
3076
3128
|
}, []);
|
|
3077
|
-
const
|
|
3078
|
-
|
|
3079
|
-
return n == null ? "" : format ? n.toLocaleString() : String(n);
|
|
3080
|
-
})();
|
|
3129
|
+
const current2 = committedRef.current;
|
|
3130
|
+
const shown = editing || !valid ? text : parsed == null ? "" : format ? parsed.toLocaleString() : String(parsed);
|
|
3081
3131
|
const shownError = error || invalidMessage || void 0;
|
|
3082
3132
|
const box = ["fd-input", "fd-input-num", shownError ? "fd-input-error" : "", disabled ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
|
|
3083
3133
|
const fieldName = label || ariaLabel;
|
|
3084
3134
|
const stepperName = (verb) => fieldName ? verb + " " + fieldName : verb;
|
|
3135
|
+
const noRange = lo > hi;
|
|
3085
3136
|
return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: ["fd-field", className].filter(Boolean).join(" "), style, children: [
|
|
3086
3137
|
label ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(FieldLabel, { htmlFor: fieldId, required, children: label }) : null,
|
|
3087
|
-
/* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: box,
|
|
3138
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: box, children: [
|
|
3088
3139
|
prefix ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "fd-input-affix", children: prefix }) : null,
|
|
3089
3140
|
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
3090
3141
|
"input",
|
|
@@ -3097,52 +3148,54 @@ function NumberInput({
|
|
|
3097
3148
|
value: shown,
|
|
3098
3149
|
"aria-invalid": shownError ? "true" : void 0,
|
|
3099
3150
|
"aria-describedby": shownError ? errorId : void 0,
|
|
3100
|
-
onFocus: () =>
|
|
3151
|
+
onFocus: () => {
|
|
3152
|
+
focusedRef.current = true;
|
|
3153
|
+
setEditing(true);
|
|
3154
|
+
},
|
|
3101
3155
|
onBlur: () => {
|
|
3156
|
+
focusedRef.current = false;
|
|
3102
3157
|
setEditing(false);
|
|
3103
|
-
if (text.trim() === "") fire(void 0);
|
|
3158
|
+
if (text.trim() === "" && committedRef.current != null) fire(void 0);
|
|
3104
3159
|
},
|
|
3105
3160
|
onChange: (e) => {
|
|
3106
3161
|
const raw = e.target.value;
|
|
3107
3162
|
setText(raw);
|
|
3108
|
-
|
|
3109
|
-
setInvalidMessage(null);
|
|
3110
|
-
fire(void 0);
|
|
3111
|
-
return;
|
|
3112
|
-
}
|
|
3113
|
-
const n = num(raw);
|
|
3163
|
+
const n = parseNumberText(raw);
|
|
3114
3164
|
if (n == null) {
|
|
3115
|
-
|
|
3165
|
+
if (committedRef.current != null) fire(void 0);
|
|
3116
3166
|
return;
|
|
3117
3167
|
}
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
if (!message) fire(n);
|
|
3168
|
+
if (Number.isNaN(n) || integer && !Number.isInteger(n) || n < lo || n > hi) return;
|
|
3169
|
+
fire(n);
|
|
3121
3170
|
},
|
|
3122
3171
|
onKeyDown: (e) => {
|
|
3123
3172
|
if (e.key === "ArrowUp") {
|
|
3124
3173
|
e.preventDefault();
|
|
3125
|
-
bump(1, e.shiftKey ?
|
|
3174
|
+
bump(1, e.shiftKey ? big : step);
|
|
3126
3175
|
} else if (e.key === "ArrowDown") {
|
|
3127
3176
|
e.preventDefault();
|
|
3128
|
-
bump(-1, e.shiftKey ?
|
|
3177
|
+
bump(-1, e.shiftKey ? big : step);
|
|
3129
3178
|
}
|
|
3130
3179
|
}
|
|
3131
3180
|
}
|
|
3132
3181
|
),
|
|
3133
3182
|
suffix ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "fd-input-affix", children: suffix }) : null,
|
|
3134
|
-
/* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "fd-
|
|
3183
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "fd-input-steppers", children: [
|
|
3135
3184
|
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
3136
3185
|
"button",
|
|
3137
3186
|
{
|
|
3138
3187
|
type: "button",
|
|
3139
3188
|
className: "fd-stepbtn",
|
|
3140
3189
|
"aria-label": stepperName("Decrease"),
|
|
3141
|
-
disabled: disabled ||
|
|
3190
|
+
disabled: disabled || noRange || current2 != null && current2 <= lo,
|
|
3142
3191
|
onPointerDown: () => hold(-1),
|
|
3143
3192
|
onPointerUp: release,
|
|
3144
3193
|
onPointerLeave: release,
|
|
3145
|
-
|
|
3194
|
+
onPointerCancel: release,
|
|
3195
|
+
onClick: (e) => {
|
|
3196
|
+
if (e.detail === 0) bump(-1, step);
|
|
3197
|
+
},
|
|
3198
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("i", { className: "ph ph-minus", "aria-hidden": "true" })
|
|
3146
3199
|
}
|
|
3147
3200
|
),
|
|
3148
3201
|
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
@@ -3151,16 +3204,21 @@ function NumberInput({
|
|
|
3151
3204
|
type: "button",
|
|
3152
3205
|
className: "fd-stepbtn",
|
|
3153
3206
|
"aria-label": stepperName("Increase"),
|
|
3154
|
-
disabled: disabled ||
|
|
3207
|
+
disabled: disabled || noRange || current2 != null && current2 >= hi,
|
|
3155
3208
|
onPointerDown: () => hold(1),
|
|
3156
3209
|
onPointerUp: release,
|
|
3157
3210
|
onPointerLeave: release,
|
|
3158
|
-
|
|
3211
|
+
onPointerCancel: release,
|
|
3212
|
+
onClick: (e) => {
|
|
3213
|
+
if (e.detail === 0) bump(1, step);
|
|
3214
|
+
},
|
|
3215
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("i", { className: "ph ph-plus", "aria-hidden": "true" })
|
|
3159
3216
|
}
|
|
3160
3217
|
)
|
|
3161
3218
|
] })
|
|
3162
3219
|
] }),
|
|
3163
|
-
|
|
3220
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { id: errorId, className: "fd-sr", role: "status", children: shownError ?? "" }),
|
|
3221
|
+
shownError ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "fd-field-error", "aria-hidden": "true", children: [
|
|
3164
3222
|
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("i", { className: "ph ph-warning-circle", "aria-hidden": "true" }),
|
|
3165
3223
|
shownError
|
|
3166
3224
|
] }) : help ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "fd-field-help", children: help }) : null
|
|
@@ -3440,16 +3498,31 @@ function isFieldSet(field, value) {
|
|
|
3440
3498
|
}
|
|
3441
3499
|
}
|
|
3442
3500
|
}
|
|
3501
|
+
var MAX_WITHIN_DAYS = 3650;
|
|
3502
|
+
function effectiveMaxDays(field) {
|
|
3503
|
+
return Math.min(field.maxDays ?? MAX_WITHIN_DAYS, MAX_WITHIN_DAYS);
|
|
3504
|
+
}
|
|
3505
|
+
function isIsoDate(v) {
|
|
3506
|
+
if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(v)) return false;
|
|
3507
|
+
const d = /* @__PURE__ */ new Date(v + "T00:00:00Z");
|
|
3508
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === v;
|
|
3509
|
+
}
|
|
3510
|
+
var isOption = (o) => !!o && typeof o.value === "string" && typeof o.label === "string";
|
|
3443
3511
|
function validateFilterValue(field, value) {
|
|
3444
3512
|
switch (field.type) {
|
|
3445
3513
|
case "text":
|
|
3514
|
+
return typeof value === "string" ? { ok: true } : { ok: false, reason: "Enter text" };
|
|
3446
3515
|
case "select":
|
|
3447
|
-
return { ok: true };
|
|
3516
|
+
return field.options.some((o) => o.value === value) ? { ok: true } : { ok: false, reason: "Pick one of the options" };
|
|
3448
3517
|
case "multiSelect": {
|
|
3449
3518
|
const v = value;
|
|
3450
3519
|
if (!v || !Array.isArray(v.values) || v.values.length === 0) {
|
|
3451
3520
|
return { ok: false, reason: "Select at least one value" };
|
|
3452
3521
|
}
|
|
3522
|
+
if (!v.values.every(isOption)) return { ok: false, reason: "Each value needs a value and a label" };
|
|
3523
|
+
if (field.maxValues != null && v.values.length > field.maxValues) {
|
|
3524
|
+
return { ok: false, reason: "Select at most " + field.maxValues + " values" };
|
|
3525
|
+
}
|
|
3453
3526
|
if (!operatorsFor(field).includes(v.operator)) {
|
|
3454
3527
|
return { ok: false, reason: "That operator isn't allowed for " + field.label };
|
|
3455
3528
|
}
|
|
@@ -3464,6 +3537,7 @@ function validateFilterValue(field, value) {
|
|
|
3464
3537
|
const hi = field.max ?? Infinity;
|
|
3465
3538
|
for (const n of [v.min, v.max]) {
|
|
3466
3539
|
if (n == null) continue;
|
|
3540
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return { ok: false, reason: "Enter a number" };
|
|
3467
3541
|
if (field.integer && !Number.isInteger(n)) return { ok: false, reason: "Enter a whole number" };
|
|
3468
3542
|
if (n < lo) return { ok: false, reason: "Must be \u2265 " + lo };
|
|
3469
3543
|
if (n > hi) return { ok: false, reason: "Must be \u2264 " + hi };
|
|
@@ -3477,46 +3551,36 @@ function validateFilterValue(field, value) {
|
|
|
3477
3551
|
const v = value;
|
|
3478
3552
|
if (!v) return { ok: false, reason: "Pick a date value" };
|
|
3479
3553
|
if (v.kind === "withinDays") {
|
|
3480
|
-
const maxDays = field
|
|
3554
|
+
const maxDays = effectiveMaxDays(field);
|
|
3481
3555
|
if (v.days == null) return { ok: false, reason: "Enter a number of days" };
|
|
3482
3556
|
if (!Number.isInteger(v.days)) return { ok: false, reason: "Enter a whole number" };
|
|
3483
3557
|
if (v.days < 1) return { ok: false, reason: "Must be \u2265 1" };
|
|
3484
3558
|
if (v.days > maxDays) return { ok: false, reason: "Must be \u2264 " + maxDays };
|
|
3485
3559
|
return { ok: true };
|
|
3486
3560
|
}
|
|
3561
|
+
if (v.kind !== "between") return { ok: false, reason: "Pick a date value" };
|
|
3487
3562
|
if (!v.from || !v.to) return { ok: false, reason: "Pick both dates" };
|
|
3563
|
+
if (!isIsoDate(v.from) || !isIsoDate(v.to)) return { ok: false, reason: "Dates must be YYYY-MM-DD" };
|
|
3488
3564
|
if (v.from > v.to) return { ok: false, reason: "The start date must be on or before the end date" };
|
|
3489
3565
|
return { ok: true };
|
|
3490
3566
|
}
|
|
3491
3567
|
}
|
|
3492
3568
|
}
|
|
3493
3569
|
function isEmptyCandidate(field, candidate) {
|
|
3494
|
-
|
|
3495
|
-
case "text":
|
|
3496
|
-
case "select":
|
|
3497
|
-
return false;
|
|
3498
|
-
case "multiSelect": {
|
|
3499
|
-
const v = candidate;
|
|
3500
|
-
return !v || !Array.isArray(v.values) || v.values.length === 0;
|
|
3501
|
-
}
|
|
3502
|
-
case "range": {
|
|
3503
|
-
const v = candidate;
|
|
3504
|
-
return !v || v.min == null && v.max == null;
|
|
3505
|
-
}
|
|
3506
|
-
case "date": {
|
|
3507
|
-
const v = candidate;
|
|
3508
|
-
if (!v) return true;
|
|
3509
|
-
return v.kind === "withinDays" ? v.days == null : !(v.from && v.to);
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3570
|
+
return field.type !== "text" && field.type !== "select" && !isFieldSet(field, candidate);
|
|
3512
3571
|
}
|
|
3513
|
-
function
|
|
3514
|
-
if (
|
|
3572
|
+
function assertValidFieldDefinition(field) {
|
|
3573
|
+
if (field.type === "multiSelect" && field.maxValues != null && (!Number.isInteger(field.maxValues) || field.maxValues < 1)) {
|
|
3574
|
+
throw new Error(
|
|
3575
|
+
'FilterBar: multiSelect field "' + field.key + '" has an invalid maxValues (' + field.maxValues + ") \u2014 must be a whole number of at least 1."
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
3578
|
+
if (field.type !== "date") return;
|
|
3515
3579
|
const defaultDays = field.defaultDays ?? 30;
|
|
3516
|
-
const maxDays = field.maxDays ??
|
|
3517
|
-
if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays >
|
|
3580
|
+
const maxDays = field.maxDays ?? MAX_WITHIN_DAYS;
|
|
3581
|
+
if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays > MAX_WITHIN_DAYS) {
|
|
3518
3582
|
throw new Error(
|
|
3519
|
-
'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to
|
|
3583
|
+
'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to " + MAX_WITHIN_DAYS + "."
|
|
3520
3584
|
);
|
|
3521
3585
|
}
|
|
3522
3586
|
if (!Number.isInteger(defaultDays) || defaultDays < 1 || defaultDays > maxDays) {
|
|
@@ -3595,12 +3659,10 @@ function MultiSelectPanel({
|
|
|
3595
3659
|
mountedRef.current = false;
|
|
3596
3660
|
};
|
|
3597
3661
|
}, []);
|
|
3598
|
-
const commit = (nextValues, nextOperator = operator) => {
|
|
3599
|
-
onCommit({ operator: nextOperator, values: nextValues });
|
|
3600
|
-
};
|
|
3662
|
+
const commit = (nextValues, nextOperator = operator) => onCommit({ operator: nextOperator, values: nextValues });
|
|
3601
3663
|
const setOperator = (op) => {
|
|
3602
3664
|
setDraftOperator(op);
|
|
3603
|
-
if (
|
|
3665
|
+
if (selectedRef.current.length > 0) commit(selectedRef.current, op);
|
|
3604
3666
|
};
|
|
3605
3667
|
const [mode2, setMode] = React24.useState("search");
|
|
3606
3668
|
const [query, setQuery] = React24.useState("");
|
|
@@ -3640,9 +3702,12 @@ function MultiSelectPanel({
|
|
|
3640
3702
|
const showError = !!field.loadOptions && asyncState.error;
|
|
3641
3703
|
const hasListbox = !showLoading && !showError && options.length > 0;
|
|
3642
3704
|
const isSelected = (o) => selected.some((s) => s.value === o.value);
|
|
3705
|
+
const atLimit = field.maxValues != null && selected.length >= field.maxValues;
|
|
3643
3706
|
const toggle = (o) => {
|
|
3644
|
-
const
|
|
3645
|
-
|
|
3707
|
+
const current2 = selectedRef.current;
|
|
3708
|
+
const on = current2.some((s) => s.value === o.value);
|
|
3709
|
+
if (!on && field.maxValues != null && current2.length >= field.maxValues) return;
|
|
3710
|
+
commit(on ? current2.filter((s) => s.value !== o.value) : [...current2, o]);
|
|
3646
3711
|
};
|
|
3647
3712
|
const nav = useListboxNav({ count: options.length, onSelect: (i) => toggle(options[i]) });
|
|
3648
3713
|
useListboxScrollIntoView(nav.active, listRef, hasListbox);
|
|
@@ -3650,8 +3715,7 @@ function MultiSelectPanel({
|
|
|
3650
3715
|
nav.setActive(0);
|
|
3651
3716
|
}, [query, options.length]);
|
|
3652
3717
|
const activeId = hasListbox && options[nav.active] ? listId + "-opt-" + nav.active : void 0;
|
|
3653
|
-
const
|
|
3654
|
-
const liveMessage = showLoading ? "Loading\u2026" : showError ? "Couldn't load options." : options.length === 0 ? "No matches" : "";
|
|
3718
|
+
const liveMessage = showLoading ? "Loading\u2026" : showError ? "Couldn't load options." : options.length === 0 ? "No matches" : atLimit ? "Limit reached: at most " + field.maxValues + " values" : "";
|
|
3655
3719
|
const [pasteText, setPasteText] = React24.useState("");
|
|
3656
3720
|
const [pasteLoading, setPasteLoading] = React24.useState(false);
|
|
3657
3721
|
const [unmatchedTokens, setUnmatchedTokens] = React24.useState([]);
|
|
@@ -3672,9 +3736,17 @@ function MultiSelectPanel({
|
|
|
3672
3736
|
setUnmatchedTokens(result.unmatched);
|
|
3673
3737
|
const merged = new Map(selectedRef.current.map((s) => [s.value, s]));
|
|
3674
3738
|
for (const m of result.matched) merged.set(m.value, { value: m.value, label: m.label });
|
|
3675
|
-
|
|
3739
|
+
if (field.maxValues != null && merged.size > field.maxValues) {
|
|
3740
|
+
setPasteError("This filter allows at most " + field.maxValues + " values; adding this list would make " + merged.size + ".");
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
const applied = commit(Array.from(merged.values()), operatorRef.current);
|
|
3744
|
+
if (!applied.ok) {
|
|
3745
|
+
setPasteError(applied.reason);
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
3676
3748
|
setPasteText("");
|
|
3677
|
-
}
|
|
3749
|
+
}, (err) => {
|
|
3678
3750
|
if (!mountedRef.current) return;
|
|
3679
3751
|
setPasteLoading(false);
|
|
3680
3752
|
setPasteError(err instanceof Error ? err.message : "Could not resolve the pasted list.");
|
|
@@ -3753,14 +3825,14 @@ function MultiSelectPanel({
|
|
|
3753
3825
|
selected.length,
|
|
3754
3826
|
" matched"
|
|
3755
3827
|
] }),
|
|
3756
|
-
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-row fd-wrap fd-gap-xs", children: selected.map((v) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Tag, { onRemove: () => commit(
|
|
3828
|
+
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-row fd-wrap fd-gap-xs", children: selected.map((v) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Tag, { onRemove: () => commit(selectedRef.current.filter((s) => s.value !== v.value)), children: v.label }, v.value)) })
|
|
3757
3829
|
] }) : null
|
|
3758
3830
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "fd-stack fd-gap-sm", children: [
|
|
3759
3831
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
|
|
3760
3832
|
"span",
|
|
3761
3833
|
{
|
|
3762
3834
|
className: "fd-row fd-gap-xs" + (liveMessage && !showError ? " fd-muted" : ""),
|
|
3763
|
-
role:
|
|
3835
|
+
role: "status",
|
|
3764
3836
|
children: [
|
|
3765
3837
|
showLoading ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-spinner", "aria-hidden": "true" }) : null,
|
|
3766
3838
|
liveMessage ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-body-sm", children: liveMessage }) : null
|
|
@@ -3770,6 +3842,7 @@ function MultiSelectPanel({
|
|
|
3770
3842
|
showError ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: () => setRetryTick((n) => n + 1), children: "Retry" }) : null,
|
|
3771
3843
|
hasListbox ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { id: listId, role: "listbox", "aria-label": field.label, "aria-multiselectable": "true", ref: listRef, className: "fd-pop-list", children: options.map((o, i) => {
|
|
3772
3844
|
const on = isSelected(o);
|
|
3845
|
+
const blocked = !on && atLimit;
|
|
3773
3846
|
return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
|
|
3774
3847
|
"div",
|
|
3775
3848
|
{
|
|
@@ -3778,6 +3851,7 @@ function MultiSelectPanel({
|
|
|
3778
3851
|
role: "option",
|
|
3779
3852
|
tabIndex: -1,
|
|
3780
3853
|
"aria-selected": on,
|
|
3854
|
+
"aria-disabled": blocked ? "true" : void 0,
|
|
3781
3855
|
className: ["fd-opt", i === nav.active ? "is-active" : "", on ? "is-selected" : ""].filter(Boolean).join(" "),
|
|
3782
3856
|
onMouseEnter: () => nav.setActive(i),
|
|
3783
3857
|
onClick: () => toggle(o),
|
|
@@ -3807,6 +3881,34 @@ function MultiSelectPanel({
|
|
|
3807
3881
|
}
|
|
3808
3882
|
);
|
|
3809
3883
|
}
|
|
3884
|
+
function useTextValidity(onFlip) {
|
|
3885
|
+
const invalidRef = React24.useRef(/* @__PURE__ */ new Set());
|
|
3886
|
+
const [, rerender] = React24.useReducer((n) => n + 1, 0);
|
|
3887
|
+
const onFlipRef = React24.useRef(onFlip);
|
|
3888
|
+
onFlipRef.current = onFlip;
|
|
3889
|
+
const record = (name, valid) => {
|
|
3890
|
+
const invalid = invalidRef.current;
|
|
3891
|
+
if (valid !== invalid.has(name)) return false;
|
|
3892
|
+
if (valid) invalid.delete(name);
|
|
3893
|
+
else invalid.add(name);
|
|
3894
|
+
rerender();
|
|
3895
|
+
return true;
|
|
3896
|
+
};
|
|
3897
|
+
return {
|
|
3898
|
+
allValid: () => invalidRef.current.size === 0,
|
|
3899
|
+
report: (name) => (valid) => {
|
|
3900
|
+
if (record(name, valid)) onFlipRef.current();
|
|
3901
|
+
},
|
|
3902
|
+
/** For an input that is about to unmount: it has nothing left to show. */
|
|
3903
|
+
forget: (name) => {
|
|
3904
|
+
record(name, true);
|
|
3905
|
+
}
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
function applicable(field, candidate, textValid) {
|
|
3909
|
+
if (!textValid || candidate === void 0) return void 0;
|
|
3910
|
+
return validateFilterValue(field, candidate).ok ? candidate : void 0;
|
|
3911
|
+
}
|
|
3810
3912
|
function RangePanel({
|
|
3811
3913
|
field,
|
|
3812
3914
|
value,
|
|
@@ -3814,18 +3916,24 @@ function RangePanel({
|
|
|
3814
3916
|
onCommit,
|
|
3815
3917
|
onClose
|
|
3816
3918
|
}) {
|
|
3817
|
-
const [
|
|
3818
|
-
const
|
|
3819
|
-
const
|
|
3820
|
-
|
|
3919
|
+
const [draft, setDraft] = React24.useState({ min: value?.min, max: value?.max });
|
|
3920
|
+
const draftRef = React24.useRef(draft);
|
|
3921
|
+
const publish = () => {
|
|
3922
|
+
const { min, max } = draftRef.current;
|
|
3923
|
+
onCommit(applicable(field, min == null && max == null ? void 0 : { min, max }, validity.allValid()));
|
|
3924
|
+
};
|
|
3925
|
+
const validity = useTextValidity(publish);
|
|
3926
|
+
const draftMin = draft.min;
|
|
3927
|
+
const draftMax = draft.max;
|
|
3821
3928
|
const isEmpty = draftMin == null && draftMax == null;
|
|
3822
3929
|
const validation = isEmpty ? { ok: true } : validateFilterValue(field, { min: draftMin, max: draftMax });
|
|
3823
3930
|
const comboError = !isEmpty && !validation.ok ? validation.reason : void 0;
|
|
3824
|
-
const doneDisabled = !
|
|
3825
|
-
const apply = (
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3931
|
+
const doneDisabled = !validity.allValid() || !!comboError;
|
|
3932
|
+
const apply = (patch) => {
|
|
3933
|
+
const next = { ...draftRef.current, ...patch };
|
|
3934
|
+
draftRef.current = next;
|
|
3935
|
+
setDraft(next);
|
|
3936
|
+
publish();
|
|
3829
3937
|
};
|
|
3830
3938
|
return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
3831
3939
|
Popover,
|
|
@@ -3849,8 +3957,8 @@ function RangePanel({
|
|
|
3849
3957
|
step: field.step,
|
|
3850
3958
|
integer: field.integer,
|
|
3851
3959
|
placeholder: field.minPlaceholder,
|
|
3852
|
-
onChange: (e) => apply(e.target.value
|
|
3853
|
-
onValidityChange:
|
|
3960
|
+
onChange: (e) => apply({ min: e.target.value }),
|
|
3961
|
+
onValidityChange: validity.report("min")
|
|
3854
3962
|
}
|
|
3855
3963
|
),
|
|
3856
3964
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
@@ -3864,8 +3972,8 @@ function RangePanel({
|
|
|
3864
3972
|
integer: field.integer,
|
|
3865
3973
|
error: comboError,
|
|
3866
3974
|
placeholder: field.maxPlaceholder,
|
|
3867
|
-
onChange: (e) => apply(
|
|
3868
|
-
onValidityChange:
|
|
3975
|
+
onChange: (e) => apply({ max: e.target.value }),
|
|
3976
|
+
onValidityChange: validity.report("max")
|
|
3869
3977
|
}
|
|
3870
3978
|
)
|
|
3871
3979
|
] })
|
|
@@ -3880,31 +3988,48 @@ function DatePanel({
|
|
|
3880
3988
|
onClose
|
|
3881
3989
|
}) {
|
|
3882
3990
|
const defaultDays = field.defaultDays ?? 30;
|
|
3883
|
-
const maxDays = field
|
|
3991
|
+
const maxDays = effectiveMaxDays(field);
|
|
3884
3992
|
const [kind, setKind] = React24.useState(value?.kind ?? "withinDays");
|
|
3885
3993
|
const [daysDraft, setDaysDraft] = React24.useState(
|
|
3886
3994
|
value?.kind === "withinDays" ? value.days : defaultDays
|
|
3887
3995
|
);
|
|
3996
|
+
const daysRef = React24.useRef(daysDraft);
|
|
3888
3997
|
const [between, setBetween] = React24.useState(
|
|
3889
3998
|
value?.kind === "between" ? { from: value.from, to: value.to } : { from: "", to: "" }
|
|
3890
3999
|
);
|
|
4000
|
+
const shownWithinDays = () => {
|
|
4001
|
+
const days = daysRef.current;
|
|
4002
|
+
return applicable(field, days == null ? void 0 : { kind: "withinDays", days }, validity.allValid());
|
|
4003
|
+
};
|
|
4004
|
+
const validity = useTextValidity(() => onCommit(shownWithinDays()));
|
|
3891
4005
|
const applyWithinDays = (days) => {
|
|
4006
|
+
daysRef.current = days;
|
|
3892
4007
|
setDaysDraft(days);
|
|
3893
|
-
onCommit(
|
|
4008
|
+
onCommit(shownWithinDays());
|
|
3894
4009
|
};
|
|
3895
4010
|
const applyBetween = (from, to) => {
|
|
3896
4011
|
setBetween({ from, to });
|
|
3897
|
-
onCommit(from && to ? { kind: "between", from, to } : void 0);
|
|
4012
|
+
onCommit(applicable(field, from && to ? { kind: "between", from, to } : void 0, true));
|
|
3898
4013
|
};
|
|
3899
4014
|
const switchKind = (next) => {
|
|
3900
4015
|
if (next === kind) return;
|
|
3901
4016
|
setKind(next);
|
|
3902
|
-
if (next === "between")
|
|
3903
|
-
|
|
4017
|
+
if (next === "between") {
|
|
4018
|
+
setBetween({ from: "", to: "" });
|
|
4019
|
+
validity.forget("days");
|
|
4020
|
+
} else {
|
|
4021
|
+
daysRef.current = defaultDays;
|
|
4022
|
+
setDaysDraft(defaultDays);
|
|
4023
|
+
}
|
|
3904
4024
|
onCommit(void 0);
|
|
3905
4025
|
};
|
|
4026
|
+
const doneDisabled = kind === "withinDays" && !validity.allValid();
|
|
3906
4027
|
const confirm = () => {
|
|
3907
|
-
if (
|
|
4028
|
+
if (doneDisabled) return;
|
|
4029
|
+
if (kind === "withinDays") {
|
|
4030
|
+
const shown = shownWithinDays();
|
|
4031
|
+
if (shown) onCommit(shown);
|
|
4032
|
+
}
|
|
3908
4033
|
onClose();
|
|
3909
4034
|
};
|
|
3910
4035
|
return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
@@ -3917,7 +4042,7 @@ function DatePanel({
|
|
|
3917
4042
|
offset: 6,
|
|
3918
4043
|
width: PANEL_WIDTH.date,
|
|
3919
4044
|
label: field.label,
|
|
3920
|
-
footer: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: confirm, children: "Done" }),
|
|
4045
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: confirm, disabled: doneDisabled, children: "Done" }),
|
|
3921
4046
|
children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "fd-stack fd-gap-sm", children: [
|
|
3922
4047
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
3923
4048
|
SegmentedControl,
|
|
@@ -3931,7 +4056,19 @@ function DatePanel({
|
|
|
3931
4056
|
onChange: (k) => switchKind(k)
|
|
3932
4057
|
}
|
|
3933
4058
|
),
|
|
3934
|
-
kind === "withinDays" ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
4059
|
+
kind === "withinDays" ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
4060
|
+
NumberInput,
|
|
4061
|
+
{
|
|
4062
|
+
label: "Days",
|
|
4063
|
+
min: 1,
|
|
4064
|
+
max: maxDays,
|
|
4065
|
+
step: 1,
|
|
4066
|
+
integer: true,
|
|
4067
|
+
value: daysDraft ?? "",
|
|
4068
|
+
onChange: (e) => applyWithinDays(e.target.value),
|
|
4069
|
+
onValidityChange: validity.report("days")
|
|
4070
|
+
}
|
|
4071
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
3935
4072
|
DatePicker,
|
|
3936
4073
|
{
|
|
3937
4074
|
range: true,
|
|
@@ -3951,19 +4088,38 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3951
4088
|
const textInputRef = React24.useRef(null);
|
|
3952
4089
|
const optionsRef = React24.useRef(null);
|
|
3953
4090
|
if (process.env.NODE_ENV !== "production") {
|
|
3954
|
-
for (const f of fields)
|
|
4091
|
+
for (const f of fields) assertValidFieldDefinition(f);
|
|
3955
4092
|
}
|
|
4093
|
+
const latest = React24.useRef({ fields, filters, onFilterChange, onFilterRemove });
|
|
4094
|
+
const filtersProp = React24.useRef(filters);
|
|
4095
|
+
if (filters !== filtersProp.current) {
|
|
4096
|
+
filtersProp.current = filters;
|
|
4097
|
+
latest.current.filters = filters;
|
|
4098
|
+
}
|
|
4099
|
+
latest.current.fields = fields;
|
|
4100
|
+
latest.current.onFilterChange = onFilterChange;
|
|
4101
|
+
latest.current.onFilterRemove = onFilterRemove;
|
|
3956
4102
|
const commit = (field, candidate) => {
|
|
3957
|
-
|
|
3958
|
-
|
|
4103
|
+
const current2 = latest.current.fields.find((f) => f.key === field.key);
|
|
4104
|
+
if (!current2) return { ok: false, reason: "This filter is no longer available" };
|
|
4105
|
+
if (isEmptyCandidate(current2, candidate)) {
|
|
4106
|
+
removeField(current2);
|
|
3959
4107
|
return { ok: true };
|
|
3960
4108
|
}
|
|
3961
|
-
const result = validateFilterValue(
|
|
3962
|
-
if (result.ok)
|
|
4109
|
+
const result = validateFilterValue(current2, candidate);
|
|
4110
|
+
if (result.ok) {
|
|
4111
|
+
latest.current.filters = { ...latest.current.filters, [current2.key]: candidate };
|
|
4112
|
+
latest.current.onFilterChange(current2.key, candidate);
|
|
4113
|
+
}
|
|
3963
4114
|
return result;
|
|
3964
4115
|
};
|
|
3965
4116
|
const removeField = (field) => {
|
|
3966
|
-
|
|
4117
|
+
const { filters: now, onFilterRemove: remove } = latest.current;
|
|
4118
|
+
if (!isFieldSet(field, now[field.key])) return;
|
|
4119
|
+
const next = { ...now };
|
|
4120
|
+
delete next[field.key];
|
|
4121
|
+
latest.current.filters = next;
|
|
4122
|
+
remove(field.key);
|
|
3967
4123
|
};
|
|
3968
4124
|
const editingField = panel && panel.kind === "edit" ? fields.find((f) => f.key === panel.key) : void 0;
|
|
3969
4125
|
const startEditing = (key) => setPanel({ kind: "edit", key });
|
|
@@ -13958,6 +14114,7 @@ function useDocumentTitle(routeTitle, appName) {
|
|
|
13958
14114
|
useServerTable,
|
|
13959
14115
|
useSessionContext,
|
|
13960
14116
|
useStagedFiles,
|
|
14117
|
+
validateFilterValue,
|
|
13961
14118
|
visibleMessages
|
|
13962
14119
|
});
|
|
13963
14120
|
//# sourceMappingURL=index.cjs.map
|