@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/README.md
CHANGED
|
@@ -81,7 +81,7 @@ pnpm run docs
|
|
|
81
81
|
pnpm run build # tsup — emits dist/{index.js,index.cjs,index.d.ts}
|
|
82
82
|
pnpm run typecheck # tsc --noEmit
|
|
83
83
|
pnpm run lint # eslint .
|
|
84
|
-
pnpm run test # vitest run
|
|
84
|
+
pnpm run test # tsup, then vitest run (some tests check the built dist/)
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
## Releasing
|
|
@@ -112,9 +112,9 @@ bash scripts/release.sh <new-version> "<summary>"
|
|
|
112
112
|
- fetches `origin` and checks out `origin/main`'s exact tip — never unmerged
|
|
113
113
|
work;
|
|
114
114
|
- installs with `pnpm install --frozen-lockfile`;
|
|
115
|
-
- runs the gates: `
|
|
116
|
-
|
|
117
|
-
has that script;
|
|
115
|
+
- runs the gates: `tsup` (first, since some tests check the built `dist/`),
|
|
116
|
+
`vitest run`, `tsc --noEmit`, `tsc --noEmit -p tsconfig.docs.json`, the docs
|
|
117
|
+
`vite build`, and `lint` if the repo has that script;
|
|
118
118
|
- refuses `<new-version>` unless it is greater than both the working
|
|
119
119
|
checkout's `package.json` version and npm's currently published `latest`;
|
|
120
120
|
- bumps `package.json`, commits `chore(release): <new-version> — <summary>`,
|
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
|
|
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);
|
|
3040
3086
|
React21.useEffect(() => {
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
const bump = (
|
|
3052
|
-
const
|
|
3053
|
-
|
|
3054
|
-
let next = cur + dir * amount;
|
|
3055
|
-
if (integer) next = Math.round(next);
|
|
3056
|
-
next = clamp(next);
|
|
3057
|
-
setInvalidMessage(null);
|
|
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,11 +3204,15 @@ 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
|
] })
|
|
@@ -3440,16 +3497,31 @@ function isFieldSet(field, value) {
|
|
|
3440
3497
|
}
|
|
3441
3498
|
}
|
|
3442
3499
|
}
|
|
3500
|
+
var MAX_WITHIN_DAYS = 3650;
|
|
3501
|
+
function effectiveMaxDays(field) {
|
|
3502
|
+
return Math.min(field.maxDays ?? MAX_WITHIN_DAYS, MAX_WITHIN_DAYS);
|
|
3503
|
+
}
|
|
3504
|
+
function isIsoDate(v) {
|
|
3505
|
+
if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(v)) return false;
|
|
3506
|
+
const d = /* @__PURE__ */ new Date(v + "T00:00:00Z");
|
|
3507
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === v;
|
|
3508
|
+
}
|
|
3509
|
+
var isOption = (o) => !!o && typeof o.value === "string" && typeof o.label === "string";
|
|
3443
3510
|
function validateFilterValue(field, value) {
|
|
3444
3511
|
switch (field.type) {
|
|
3445
3512
|
case "text":
|
|
3513
|
+
return typeof value === "string" ? { ok: true } : { ok: false, reason: "Enter text" };
|
|
3446
3514
|
case "select":
|
|
3447
|
-
return { ok: true };
|
|
3515
|
+
return field.options.some((o) => o.value === value) ? { ok: true } : { ok: false, reason: "Pick one of the options" };
|
|
3448
3516
|
case "multiSelect": {
|
|
3449
3517
|
const v = value;
|
|
3450
3518
|
if (!v || !Array.isArray(v.values) || v.values.length === 0) {
|
|
3451
3519
|
return { ok: false, reason: "Select at least one value" };
|
|
3452
3520
|
}
|
|
3521
|
+
if (!v.values.every(isOption)) return { ok: false, reason: "Each value needs a value and a label" };
|
|
3522
|
+
if (field.maxValues != null && v.values.length > field.maxValues) {
|
|
3523
|
+
return { ok: false, reason: "Select at most " + field.maxValues + " values" };
|
|
3524
|
+
}
|
|
3453
3525
|
if (!operatorsFor(field).includes(v.operator)) {
|
|
3454
3526
|
return { ok: false, reason: "That operator isn't allowed for " + field.label };
|
|
3455
3527
|
}
|
|
@@ -3464,6 +3536,7 @@ function validateFilterValue(field, value) {
|
|
|
3464
3536
|
const hi = field.max ?? Infinity;
|
|
3465
3537
|
for (const n of [v.min, v.max]) {
|
|
3466
3538
|
if (n == null) continue;
|
|
3539
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return { ok: false, reason: "Enter a number" };
|
|
3467
3540
|
if (field.integer && !Number.isInteger(n)) return { ok: false, reason: "Enter a whole number" };
|
|
3468
3541
|
if (n < lo) return { ok: false, reason: "Must be \u2265 " + lo };
|
|
3469
3542
|
if (n > hi) return { ok: false, reason: "Must be \u2264 " + hi };
|
|
@@ -3477,46 +3550,36 @@ function validateFilterValue(field, value) {
|
|
|
3477
3550
|
const v = value;
|
|
3478
3551
|
if (!v) return { ok: false, reason: "Pick a date value" };
|
|
3479
3552
|
if (v.kind === "withinDays") {
|
|
3480
|
-
const maxDays = field
|
|
3553
|
+
const maxDays = effectiveMaxDays(field);
|
|
3481
3554
|
if (v.days == null) return { ok: false, reason: "Enter a number of days" };
|
|
3482
3555
|
if (!Number.isInteger(v.days)) return { ok: false, reason: "Enter a whole number" };
|
|
3483
3556
|
if (v.days < 1) return { ok: false, reason: "Must be \u2265 1" };
|
|
3484
3557
|
if (v.days > maxDays) return { ok: false, reason: "Must be \u2264 " + maxDays };
|
|
3485
3558
|
return { ok: true };
|
|
3486
3559
|
}
|
|
3560
|
+
if (v.kind !== "between") return { ok: false, reason: "Pick a date value" };
|
|
3487
3561
|
if (!v.from || !v.to) return { ok: false, reason: "Pick both dates" };
|
|
3562
|
+
if (!isIsoDate(v.from) || !isIsoDate(v.to)) return { ok: false, reason: "Dates must be YYYY-MM-DD" };
|
|
3488
3563
|
if (v.from > v.to) return { ok: false, reason: "The start date must be on or before the end date" };
|
|
3489
3564
|
return { ok: true };
|
|
3490
3565
|
}
|
|
3491
3566
|
}
|
|
3492
3567
|
}
|
|
3493
3568
|
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
|
-
}
|
|
3569
|
+
return field.type !== "text" && field.type !== "select" && !isFieldSet(field, candidate);
|
|
3512
3570
|
}
|
|
3513
|
-
function
|
|
3514
|
-
if (
|
|
3571
|
+
function assertValidFieldDefinition(field) {
|
|
3572
|
+
if (field.type === "multiSelect" && field.maxValues != null && (!Number.isInteger(field.maxValues) || field.maxValues < 1)) {
|
|
3573
|
+
throw new Error(
|
|
3574
|
+
'FilterBar: multiSelect field "' + field.key + '" has an invalid maxValues (' + field.maxValues + ") \u2014 must be a whole number of at least 1."
|
|
3575
|
+
);
|
|
3576
|
+
}
|
|
3577
|
+
if (field.type !== "date") return;
|
|
3515
3578
|
const defaultDays = field.defaultDays ?? 30;
|
|
3516
|
-
const maxDays = field.maxDays ??
|
|
3517
|
-
if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays >
|
|
3579
|
+
const maxDays = field.maxDays ?? MAX_WITHIN_DAYS;
|
|
3580
|
+
if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays > MAX_WITHIN_DAYS) {
|
|
3518
3581
|
throw new Error(
|
|
3519
|
-
'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to
|
|
3582
|
+
'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to " + MAX_WITHIN_DAYS + "."
|
|
3520
3583
|
);
|
|
3521
3584
|
}
|
|
3522
3585
|
if (!Number.isInteger(defaultDays) || defaultDays < 1 || defaultDays > maxDays) {
|
|
@@ -3600,7 +3663,7 @@ function MultiSelectPanel({
|
|
|
3600
3663
|
};
|
|
3601
3664
|
const setOperator = (op) => {
|
|
3602
3665
|
setDraftOperator(op);
|
|
3603
|
-
if (
|
|
3666
|
+
if (selectedRef.current.length > 0) commit(selectedRef.current, op);
|
|
3604
3667
|
};
|
|
3605
3668
|
const [mode2, setMode] = React24.useState("search");
|
|
3606
3669
|
const [query, setQuery] = React24.useState("");
|
|
@@ -3640,9 +3703,12 @@ function MultiSelectPanel({
|
|
|
3640
3703
|
const showError = !!field.loadOptions && asyncState.error;
|
|
3641
3704
|
const hasListbox = !showLoading && !showError && options.length > 0;
|
|
3642
3705
|
const isSelected = (o) => selected.some((s) => s.value === o.value);
|
|
3706
|
+
const atLimit = field.maxValues != null && selected.length >= field.maxValues;
|
|
3643
3707
|
const toggle = (o) => {
|
|
3644
|
-
const
|
|
3645
|
-
|
|
3708
|
+
const current2 = selectedRef.current;
|
|
3709
|
+
const on = current2.some((s) => s.value === o.value);
|
|
3710
|
+
if (!on && field.maxValues != null && current2.length >= field.maxValues) return;
|
|
3711
|
+
commit(on ? current2.filter((s) => s.value !== o.value) : [...current2, o]);
|
|
3646
3712
|
};
|
|
3647
3713
|
const nav = useListboxNav({ count: options.length, onSelect: (i) => toggle(options[i]) });
|
|
3648
3714
|
useListboxScrollIntoView(nav.active, listRef, hasListbox);
|
|
@@ -3650,8 +3716,7 @@ function MultiSelectPanel({
|
|
|
3650
3716
|
nav.setActive(0);
|
|
3651
3717
|
}, [query, options.length]);
|
|
3652
3718
|
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" : "";
|
|
3719
|
+
const liveMessage = showLoading ? "Loading\u2026" : showError ? "Couldn't load options." : options.length === 0 ? "No matches" : atLimit ? "Limit reached: at most " + field.maxValues + " values" : "";
|
|
3655
3720
|
const [pasteText, setPasteText] = React24.useState("");
|
|
3656
3721
|
const [pasteLoading, setPasteLoading] = React24.useState(false);
|
|
3657
3722
|
const [unmatchedTokens, setUnmatchedTokens] = React24.useState([]);
|
|
@@ -3672,9 +3737,13 @@ function MultiSelectPanel({
|
|
|
3672
3737
|
setUnmatchedTokens(result.unmatched);
|
|
3673
3738
|
const merged = new Map(selectedRef.current.map((s) => [s.value, s]));
|
|
3674
3739
|
for (const m of result.matched) merged.set(m.value, { value: m.value, label: m.label });
|
|
3740
|
+
if (field.maxValues != null && merged.size > field.maxValues) {
|
|
3741
|
+
setPasteError("This filter allows at most " + field.maxValues + " values; adding this list would make " + merged.size + ".");
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3675
3744
|
commit(Array.from(merged.values()), operatorRef.current);
|
|
3676
3745
|
setPasteText("");
|
|
3677
|
-
}
|
|
3746
|
+
}, (err) => {
|
|
3678
3747
|
if (!mountedRef.current) return;
|
|
3679
3748
|
setPasteLoading(false);
|
|
3680
3749
|
setPasteError(err instanceof Error ? err.message : "Could not resolve the pasted list.");
|
|
@@ -3753,14 +3822,14 @@ function MultiSelectPanel({
|
|
|
3753
3822
|
selected.length,
|
|
3754
3823
|
" matched"
|
|
3755
3824
|
] }),
|
|
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(
|
|
3825
|
+
/* @__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
3826
|
] }) : null
|
|
3758
3827
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "fd-stack fd-gap-sm", children: [
|
|
3759
3828
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
|
|
3760
3829
|
"span",
|
|
3761
3830
|
{
|
|
3762
3831
|
className: "fd-row fd-gap-xs" + (liveMessage && !showError ? " fd-muted" : ""),
|
|
3763
|
-
role:
|
|
3832
|
+
role: "status",
|
|
3764
3833
|
children: [
|
|
3765
3834
|
showLoading ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-spinner", "aria-hidden": "true" }) : null,
|
|
3766
3835
|
liveMessage ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-body-sm", children: liveMessage }) : null
|
|
@@ -3770,6 +3839,7 @@ function MultiSelectPanel({
|
|
|
3770
3839
|
showError ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: () => setRetryTick((n) => n + 1), children: "Retry" }) : null,
|
|
3771
3840
|
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
3841
|
const on = isSelected(o);
|
|
3842
|
+
const blocked = !on && atLimit;
|
|
3773
3843
|
return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
|
|
3774
3844
|
"div",
|
|
3775
3845
|
{
|
|
@@ -3778,6 +3848,7 @@ function MultiSelectPanel({
|
|
|
3778
3848
|
role: "option",
|
|
3779
3849
|
tabIndex: -1,
|
|
3780
3850
|
"aria-selected": on,
|
|
3851
|
+
"aria-disabled": blocked ? "true" : void 0,
|
|
3781
3852
|
className: ["fd-opt", i === nav.active ? "is-active" : "", on ? "is-selected" : ""].filter(Boolean).join(" "),
|
|
3782
3853
|
onMouseEnter: () => nav.setActive(i),
|
|
3783
3854
|
onClick: () => toggle(o),
|
|
@@ -3814,18 +3885,21 @@ function RangePanel({
|
|
|
3814
3885
|
onCommit,
|
|
3815
3886
|
onClose
|
|
3816
3887
|
}) {
|
|
3817
|
-
const [
|
|
3818
|
-
const
|
|
3888
|
+
const [draft, setDraft] = React24.useState({ min: value?.min, max: value?.max });
|
|
3889
|
+
const draftRef = React24.useRef(draft);
|
|
3819
3890
|
const [minTextValid, setMinTextValid] = React24.useState(true);
|
|
3820
3891
|
const [maxTextValid, setMaxTextValid] = React24.useState(true);
|
|
3892
|
+
const draftMin = draft.min;
|
|
3893
|
+
const draftMax = draft.max;
|
|
3821
3894
|
const isEmpty = draftMin == null && draftMax == null;
|
|
3822
3895
|
const validation = isEmpty ? { ok: true } : validateFilterValue(field, { min: draftMin, max: draftMax });
|
|
3823
3896
|
const comboError = !isEmpty && !validation.ok ? validation.reason : void 0;
|
|
3824
3897
|
const doneDisabled = !minTextValid || !maxTextValid || !!comboError;
|
|
3825
|
-
const apply = (
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3898
|
+
const apply = (patch) => {
|
|
3899
|
+
const next = { ...draftRef.current, ...patch };
|
|
3900
|
+
draftRef.current = next;
|
|
3901
|
+
setDraft(next);
|
|
3902
|
+
onCommit(next.min == null && next.max == null ? void 0 : next);
|
|
3829
3903
|
};
|
|
3830
3904
|
return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
3831
3905
|
Popover,
|
|
@@ -3849,7 +3923,7 @@ function RangePanel({
|
|
|
3849
3923
|
step: field.step,
|
|
3850
3924
|
integer: field.integer,
|
|
3851
3925
|
placeholder: field.minPlaceholder,
|
|
3852
|
-
onChange: (e) => apply(e.target.value
|
|
3926
|
+
onChange: (e) => apply({ min: e.target.value }),
|
|
3853
3927
|
onValidityChange: setMinTextValid
|
|
3854
3928
|
}
|
|
3855
3929
|
),
|
|
@@ -3864,7 +3938,7 @@ function RangePanel({
|
|
|
3864
3938
|
integer: field.integer,
|
|
3865
3939
|
error: comboError,
|
|
3866
3940
|
placeholder: field.maxPlaceholder,
|
|
3867
|
-
onChange: (e) => apply(
|
|
3941
|
+
onChange: (e) => apply({ max: e.target.value }),
|
|
3868
3942
|
onValidityChange: setMaxTextValid
|
|
3869
3943
|
}
|
|
3870
3944
|
)
|
|
@@ -3880,7 +3954,8 @@ function DatePanel({
|
|
|
3880
3954
|
onClose
|
|
3881
3955
|
}) {
|
|
3882
3956
|
const defaultDays = field.defaultDays ?? 30;
|
|
3883
|
-
const maxDays = field
|
|
3957
|
+
const maxDays = effectiveMaxDays(field);
|
|
3958
|
+
const [daysTextValid, setDaysTextValid] = React24.useState(true);
|
|
3884
3959
|
const [kind, setKind] = React24.useState(value?.kind ?? "withinDays");
|
|
3885
3960
|
const [daysDraft, setDaysDraft] = React24.useState(
|
|
3886
3961
|
value?.kind === "withinDays" ? value.days : defaultDays
|
|
@@ -3901,9 +3976,12 @@ function DatePanel({
|
|
|
3901
3976
|
setKind(next);
|
|
3902
3977
|
if (next === "between") setBetween({ from: "", to: "" });
|
|
3903
3978
|
else setDaysDraft(defaultDays);
|
|
3979
|
+
setDaysTextValid(true);
|
|
3904
3980
|
onCommit(void 0);
|
|
3905
3981
|
};
|
|
3982
|
+
const doneDisabled = kind === "withinDays" && !daysTextValid;
|
|
3906
3983
|
const confirm = () => {
|
|
3984
|
+
if (doneDisabled) return;
|
|
3907
3985
|
if (kind === "withinDays" && daysDraft != null) onCommit({ kind: "withinDays", days: daysDraft });
|
|
3908
3986
|
onClose();
|
|
3909
3987
|
};
|
|
@@ -3917,7 +3995,7 @@ function DatePanel({
|
|
|
3917
3995
|
offset: 6,
|
|
3918
3996
|
width: PANEL_WIDTH.date,
|
|
3919
3997
|
label: field.label,
|
|
3920
|
-
footer: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: confirm, children: "Done" }),
|
|
3998
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: confirm, disabled: doneDisabled, children: "Done" }),
|
|
3921
3999
|
children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "fd-stack fd-gap-sm", children: [
|
|
3922
4000
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
3923
4001
|
SegmentedControl,
|
|
@@ -3931,7 +4009,19 @@ function DatePanel({
|
|
|
3931
4009
|
onChange: (k) => switchKind(k)
|
|
3932
4010
|
}
|
|
3933
4011
|
),
|
|
3934
|
-
kind === "withinDays" ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
4012
|
+
kind === "withinDays" ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
4013
|
+
NumberInput,
|
|
4014
|
+
{
|
|
4015
|
+
label: "Days",
|
|
4016
|
+
min: 1,
|
|
4017
|
+
max: maxDays,
|
|
4018
|
+
step: 1,
|
|
4019
|
+
integer: true,
|
|
4020
|
+
value: daysDraft ?? "",
|
|
4021
|
+
onChange: (e) => applyWithinDays(e.target.value),
|
|
4022
|
+
onValidityChange: setDaysTextValid
|
|
4023
|
+
}
|
|
4024
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
3935
4025
|
DatePicker,
|
|
3936
4026
|
{
|
|
3937
4027
|
range: true,
|
|
@@ -3951,19 +4041,31 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
|
|
|
3951
4041
|
const textInputRef = React24.useRef(null);
|
|
3952
4042
|
const optionsRef = React24.useRef(null);
|
|
3953
4043
|
if (process.env.NODE_ENV !== "production") {
|
|
3954
|
-
for (const f of fields)
|
|
4044
|
+
for (const f of fields) assertValidFieldDefinition(f);
|
|
3955
4045
|
}
|
|
4046
|
+
const latest = React24.useRef({ fields, filters, onFilterChange, onFilterRemove });
|
|
4047
|
+
latest.current = { fields, filters, onFilterChange, onFilterRemove };
|
|
3956
4048
|
const commit = (field, candidate) => {
|
|
3957
|
-
|
|
3958
|
-
|
|
4049
|
+
const current2 = latest.current.fields.find((f) => f.key === field.key);
|
|
4050
|
+
if (!current2) return { ok: false, reason: "This filter is no longer available" };
|
|
4051
|
+
if (isEmptyCandidate(current2, candidate)) {
|
|
4052
|
+
removeField(current2);
|
|
3959
4053
|
return { ok: true };
|
|
3960
4054
|
}
|
|
3961
|
-
const result = validateFilterValue(
|
|
3962
|
-
if (result.ok)
|
|
4055
|
+
const result = validateFilterValue(current2, candidate);
|
|
4056
|
+
if (result.ok) {
|
|
4057
|
+
latest.current.filters = { ...latest.current.filters, [current2.key]: candidate };
|
|
4058
|
+
latest.current.onFilterChange(current2.key, candidate);
|
|
4059
|
+
}
|
|
3963
4060
|
return result;
|
|
3964
4061
|
};
|
|
3965
4062
|
const removeField = (field) => {
|
|
3966
|
-
|
|
4063
|
+
const { filters: now, onFilterRemove: remove } = latest.current;
|
|
4064
|
+
if (!isFieldSet(field, now[field.key])) return;
|
|
4065
|
+
const next = { ...now };
|
|
4066
|
+
delete next[field.key];
|
|
4067
|
+
latest.current.filters = next;
|
|
4068
|
+
remove(field.key);
|
|
3967
4069
|
};
|
|
3968
4070
|
const editingField = panel && panel.kind === "edit" ? fields.find((f) => f.key === panel.key) : void 0;
|
|
3969
4071
|
const startEditing = (key) => setPanel({ kind: "edit", key });
|
|
@@ -13958,6 +14060,7 @@ function useDocumentTitle(routeTitle, appName) {
|
|
|
13958
14060
|
useServerTable,
|
|
13959
14061
|
useSessionContext,
|
|
13960
14062
|
useStagedFiles,
|
|
14063
|
+
validateFilterValue,
|
|
13961
14064
|
visibleMessages
|
|
13962
14065
|
});
|
|
13963
14066
|
//# sourceMappingURL=index.cjs.map
|