From 674c690bf6ad4bb2707755426af3e2def918cc62 Mon Sep 17 00:00:00 2001 From: plamendochev Date: Fri, 6 Feb 2026 14:07:20 +0000 Subject: [PATCH 1/7] Add NumericColumnFilter component for advanced numeric filtering --- .../table/action/numeric-column-filter.tsx | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 code/src/ui/graphic/table/action/numeric-column-filter.tsx diff --git a/code/src/ui/graphic/table/action/numeric-column-filter.tsx b/code/src/ui/graphic/table/action/numeric-column-filter.tsx new file mode 100644 index 000000000..b2afa6689 --- /dev/null +++ b/code/src/ui/graphic/table/action/numeric-column-filter.tsx @@ -0,0 +1,228 @@ +import { useState } from "react"; +import Button from "ui/interaction/button"; +import SimpleSelector from "ui/interaction/dropdown/simple-selector"; +import { SelectOptionType } from "ui/interaction/dropdown/simple-selector"; +import { Icon } from "@mui/material"; + +interface NumericColumnFilterProps { + options: string[]; + label: string; + onSubmission: (_options: string[]) => void; +} + +type ComparisonOperator = | "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "between" + +type LogicOperator = "and" | "or"; + +const operators: { value: ComparisonOperator; label: string; }[] = [ + { value: "eq", label: "Equals" }, + { value: "neq", label: "Does not equal" }, + { value: "gt", label: "Greater than" }, + { value: "gte", label: "Greater than or equal to" }, + { value: "lt", label: "Less than" }, + { value: "lte", label: "Less than or equal to" }, + { value: "between", label: "Between" }, +] + +/** + * Evaluates a single numeric comparison condition. + */ +function matchesCondition(value: number, operator: ComparisonOperator, target: number, target2?: number): boolean { + switch (operator) { + case "eq": return value === target; + case "neq": return value !== target; + case "gt": return value > target; + case "gte": return value >= target; + case "lt": return value < target; + case "lte": return value <= target; + case "between": return target2 !== undefined && value >= target && value <= target2; + } +} + +/** + * A numeric column filter component that allows filtering table data using one or two + * numeric comparison conditions combined with AND/OR logic. + * + * @param {string[]} options The available column values to filter against. + * @param {string} label The name of the column. + * @param {void} onSubmission Function that submits the filtered options. + */ +export default function NumericColumnFilter(props: Readonly) { + const [value1, setValue1] = useState(""); + const [value2, setValue2] = useState(""); + const [selectedOperator1, setSelectedOperator1] = useState("eq"); + const [selectedOperator2, setSelectedOperator2] = useState("eq"); + const [logicOperator, setLogicOperator] = useState("and"); + + const hasFirstValue: boolean = value1.trim() !== ""; + const hasSecondValue: boolean = value2.trim() !== ""; + const isBetween: boolean = selectedOperator1 === "between"; + + function handleFilter(): void { + const num1: number = parseFloat(value1); + const num2: number = parseFloat(value2); + + const filtered: string[] = props.options.filter((option) => { + const numericValue: number = parseFloat(option); + if (isNaN(numericValue)) return false; + + // Between uses both values in a single condition + if (isBetween) { + if (isNaN(num1) || isNaN(num2)) return false; + return matchesCondition(numericValue, "between", num1, num2); + } + + const condition1: boolean = !isNaN(num1) + ? matchesCondition(numericValue, selectedOperator1, num1) + : true; + + if (!hasSecondValue || isNaN(num2)) { + return condition1; + } + + const condition2: boolean = matchesCondition(numericValue, selectedOperator2, num2); + + return logicOperator === "and" + ? condition1 && condition2 + : condition1 || condition2; + }); + + props.onSubmission(filtered); + } + + return ( +
+ {/* First condition */} + { + if (selected) { + setSelectedOperator1((selected as SelectOptionType).value as ComparisonOperator); + } + }} + /> +
+ + search + + { + event.preventDefault(); + event.stopPropagation(); + }} + onChange={(event) => { + setValue1(event.target.value); + }} + /> +
+ + {/* Between: show "To" input directly below "From" */} + {isBetween && ( +
+ + search + + { + event.preventDefault(); + event.stopPropagation(); + }} + onChange={(event) => { + setValue2(event.target.value); + }} + /> +
+ )} + + {/* Second condition — only shown when value1 is non-empty and not between */} + {hasFirstValue && !isBetween && ( + <> + {/* AND / OR radio toggle */} +
+ + +
+ + { + if (selected) { + setSelectedOperator2((selected as SelectOptionType).value as ComparisonOperator); + } + }} + /> +
+ + search + + { + event.preventDefault(); + event.stopPropagation(); + }} + onChange={(event) => { + setValue2(event.target.value); + }} + /> +
+ + )} + +
+ ); +} \ No newline at end of file From d78ee61adcb3aa02bf4570d070303b627c6725bd Mon Sep 17 00:00:00 2001 From: plamendochev Date: Fri, 6 Feb 2026 14:26:47 +0000 Subject: [PATCH 2/7] Add numeric filter option to HeaderCell component --- .../src/ui/graphic/table/cell/header-cell.tsx | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/code/src/ui/graphic/table/cell/header-cell.tsx b/code/src/ui/graphic/table/cell/header-cell.tsx index 8d6c8e709..8517b007b 100644 --- a/code/src/ui/graphic/table/cell/header-cell.tsx +++ b/code/src/ui/graphic/table/cell/header-cell.tsx @@ -12,6 +12,7 @@ import PopoverActionButton from "ui/interaction/action/popover/popover-button"; import SearchSelector from "ui/interaction/dropdown/search-selector"; import Tooltip from "ui/interaction/tooltip/tooltip"; import TableCell from "./table-cell"; +import NumericColumnFilter from "../action/numeric-column-filter"; interface HeaderCellProps { type: string; @@ -58,6 +59,9 @@ export default function HeaderCell(props: Readonly) { props.filters, ); + const showNumericFilter: boolean = options.length > 0 && + options.every((option) => option.trim() !== "" && !Number.isNaN(Number(option))); + return ( ) { setShowFilterDropdown(!showFilterDropdown); }} > - { - props.header.column.setFilterValue(selectedOptions); - props.table.resetRowSelection(); - props.table.resetPageIndex(); - }} - setSearchString={setSearch} - /> + + {showNumericFilter ? ( + { + props.header.column.setFilterValue(selectedOptions); + props.table.resetRowSelection(); + props.table.resetPageIndex(); + }} + /> + ) : ( + { + props.header.column.setFilterValue(selectedOptions); + props.table.resetRowSelection(); + props.table.resetPageIndex(); + }} + setSearchString={setSearch} + /> + )} {isLoading && } } From cf6759a6a1baf9134c1d826ad135e18a8db6526a Mon Sep 17 00:00:00 2001 From: plamendochev Date: Fri, 6 Feb 2026 15:14:30 +0000 Subject: [PATCH 3/7] Refactor NumericColumnFilter to use number inputs and improve value handling --- .../table/action/numeric-column-filter.tsx | 86 +++++++++---------- 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/code/src/ui/graphic/table/action/numeric-column-filter.tsx b/code/src/ui/graphic/table/action/numeric-column-filter.tsx index b2afa6689..346d1441e 100644 --- a/code/src/ui/graphic/table/action/numeric-column-filter.tsx +++ b/code/src/ui/graphic/table/action/numeric-column-filter.tsx @@ -48,48 +48,46 @@ function matchesCondition(value: number, operator: ComparisonOperator, target: n * @param {void} onSubmission Function that submits the filtered options. */ export default function NumericColumnFilter(props: Readonly) { - const [value1, setValue1] = useState(""); - const [value2, setValue2] = useState(""); + const [value1, setValue1] = useState(null); + const [value2, setValue2] = useState(null); const [selectedOperator1, setSelectedOperator1] = useState("eq"); const [selectedOperator2, setSelectedOperator2] = useState("eq"); const [logicOperator, setLogicOperator] = useState("and"); - const hasFirstValue: boolean = value1.trim() !== ""; - const hasSecondValue: boolean = value2.trim() !== ""; + const hasFirstValue: boolean = value1 !== null && !Number.isNaN(value1); + const hasSecondValue: boolean = value2 !== null && !Number.isNaN(value2); const isBetween: boolean = selectedOperator1 === "between"; - function handleFilter(): void { - const num1: number = parseFloat(value1); - const num2: number = parseFloat(value2); + const handleFilter = (): void => { + if (!hasFirstValue) return; const filtered: string[] = props.options.filter((option) => { - const numericValue: number = parseFloat(option); - if (isNaN(numericValue)) return false; + const numericValue: number = Number(option); + if (Number.isNaN(numericValue)) return false; - // Between uses both values in a single condition if (isBetween) { - if (isNaN(num1) || isNaN(num2)) return false; - return matchesCondition(numericValue, "between", num1, num2); + if (!hasSecondValue) return false; + return matchesCondition(numericValue, "between", value1!, value2!); } - const condition1: boolean = !isNaN(num1) - ? matchesCondition(numericValue, selectedOperator1, num1) - : true; + const condition1: boolean = matchesCondition(numericValue, selectedOperator1, value1!); - if (!hasSecondValue || isNaN(num2)) { - return condition1; - } + if (!hasSecondValue) return condition1; - const condition2: boolean = matchesCondition(numericValue, selectedOperator2, num2); + const condition2: boolean = matchesCondition(numericValue, selectedOperator2, value2!); - return logicOperator === "and" - ? condition1 && condition2 - : condition1 || condition2; + return logicOperator === "and" ? condition1 && condition2 : condition1 || condition2; }); props.onSubmission(filtered); } + const blockInvalidNumberKeys = (e: React.KeyboardEvent) => { + if (["e", "E", "+", "-"].includes(e.key)) { + e.preventDefault(); + } + }; + return (
{/* First condition */} @@ -108,18 +106,17 @@ export default function NumericColumnFilter(props: Readonly { - event.preventDefault(); - event.stopPropagation(); - }} - onChange={(event) => { - setValue1(event.target.value); + onKeyDown={blockInvalidNumberKeys} + onChange={(e) => { + const value = e.currentTarget.valueAsNumber; + setValue1(Number.isNaN(value) ? null : value); }} />
@@ -131,18 +128,16 @@ export default function NumericColumnFilter(props: Readonlysearch { - event.preventDefault(); - event.stopPropagation(); - }} - onChange={(event) => { - setValue2(event.target.value); + onChange={(e) => { + const value = e.currentTarget.valueAsNumber; + setValue2(Number.isNaN(value) ? null : value); }} /> @@ -193,18 +188,17 @@ export default function NumericColumnFilter(props: Readonlysearch { - event.preventDefault(); - event.stopPropagation(); - }} - onChange={(event) => { - setValue2(event.target.value); + onKeyDown={blockInvalidNumberKeys} + onChange={(e) => { + const value = e.currentTarget.valueAsNumber; + setValue2(Number.isNaN(value) ? null : value); }} /> From 4e8a35807c0a8b2cfa63d39b6939960453bc6628 Mon Sep 17 00:00:00 2001 From: plamendochev Date: Fri, 6 Feb 2026 15:25:30 +0000 Subject: [PATCH 4/7] Enhance NumericColumnFilter to support multiple 'between' conditions and improve value handling --- .../table/action/numeric-column-filter.tsx | 105 +++++++++++++----- 1 file changed, 80 insertions(+), 25 deletions(-) diff --git a/code/src/ui/graphic/table/action/numeric-column-filter.tsx b/code/src/ui/graphic/table/action/numeric-column-filter.tsx index 346d1441e..68e8a3d48 100644 --- a/code/src/ui/graphic/table/action/numeric-column-filter.tsx +++ b/code/src/ui/graphic/table/action/numeric-column-filter.tsx @@ -50,13 +50,16 @@ function matchesCondition(value: number, operator: ComparisonOperator, target: n export default function NumericColumnFilter(props: Readonly) { const [value1, setValue1] = useState(null); const [value2, setValue2] = useState(null); + const [value3, setValue3] = useState(null); const [selectedOperator1, setSelectedOperator1] = useState("eq"); const [selectedOperator2, setSelectedOperator2] = useState("eq"); const [logicOperator, setLogicOperator] = useState("and"); const hasFirstValue: boolean = value1 !== null && !Number.isNaN(value1); const hasSecondValue: boolean = value2 !== null && !Number.isNaN(value2); - const isBetween: boolean = selectedOperator1 === "between"; + const hasThirdValue: boolean = value3 !== null && !Number.isNaN(value3); + const isBetweenFirst: boolean = selectedOperator1 === "between"; + const isBetweenSecond: boolean = selectedOperator2 === "between"; const handleFilter = (): void => { if (!hasFirstValue) return; @@ -65,7 +68,7 @@ export default function NumericColumnFilter(props: Readonly { @@ -122,7 +131,7 @@ export default function NumericColumnFilter(props: Readonly {/* Between: show "To" input directly below "From" */} - {isBetween && ( + {isBetweenFirst && (
search @@ -135,6 +144,7 @@ export default function NumericColumnFilter(props: Readonly { const value = e.currentTarget.valueAsNumber; setValue2(Number.isNaN(value) ? null : value); @@ -144,7 +154,7 @@ export default function NumericColumnFilter(props: Readonly {/* AND / OR radio toggle */}
@@ -183,25 +193,68 @@ export default function NumericColumnFilter(props: Readonly -
- - search - - { - const value = e.currentTarget.valueAsNumber; - setValue2(Number.isNaN(value) ? null : value); - }} - /> -
+ {isBetweenSecond ? ( + <> +
+ + search + + { + const value = e.currentTarget.valueAsNumber; + setValue2(Number.isNaN(value) ? null : value); + }} + /> +
+
+ + search + + { + const value = e.currentTarget.valueAsNumber; + setValue3(Number.isNaN(value) ? null : value); + }} + /> +
+ + ) : ( +
+ + search + + { + const value = e.currentTarget.valueAsNumber; + setValue2(Number.isNaN(value) ? null : value); + }} + /> +
+ )} )}
- - {/* Between: show "To" input directly below "From" */} - {isBetweenFirst && ( -
- - search - - { - const value = e.currentTarget.valueAsNumber; - setValue2(Number.isNaN(value) ? null : value); - }} - /> -
- )} - - {/* Second condition — only shown when value1 is non-empty and not between */} {hasFirstValue && !isBetweenFirst && ( <> {/* AND / OR radio toggle */} @@ -183,7 +157,6 @@ export default function NumericColumnFilter(props: Readonly
- -
- - search - - { - const value = e.currentTarget.valueAsNumber; - setValue2(Number.isNaN(value) ? null : value); - }} - /> -
- {isBetweenSecond && ( -
- - search - - { - const value = e.currentTarget.valueAsNumber; - setValue3(Number.isNaN(value) ? null : value); - }} - /> -
- )} )} + {(hasFirstValue || isBetweenFirst || isBetweenSecond) && +
+ + search + + { + const value = e.currentTarget.valueAsNumber; + setValue2(Number.isNaN(value) ? null : value); + }} + /> +
+ } + {!isBetweenFirst && isBetweenSecond && ( +
+ + search + + { + const value = e.currentTarget.valueAsNumber; + setValue3(Number.isNaN(value) ? null : value); + }} + /> +
+ )}