@vuu-ui/vuu-filters 3.5.14 → 3.5.16

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 CHANGED
@@ -15,6 +15,46 @@ Vuu also supports `MultiValueFilterClause`
15
15
 
16
16
  ## ColumnFilter
17
17
 
18
+ Set `showOperatorPicker` to render a compact operator dropdown before the value.
19
+ It is opt-in; existing fixed-operator filters are unchanged.
20
+
21
+ ```tsx
22
+ <ColumnFilter
23
+ column={{ name: "price", serverDataType: "double" }}
24
+ showOperatorPicker
25
+ operators={["=", ">", ">=", "between-inclusive"]}
26
+ defaultValue=""
27
+ onCommit={handleCommit}
28
+ />
29
+ ```
30
+
31
+ `operators` replaces the default options and preserves their order. Numeric
32
+ (including scaled-decimal) and temporal columns default to `=`, `!=`, `>`, `>=`,
33
+ `<`, `<=`; text columns to `=`, `!=`, `starts`, `ends`, `contains`; boolean
34
+ columns to `=`, `!=`. Numeric and temporal filters can also opt into `between`
35
+ and `between-inclusive`. Multi-value `in` filters are not supported by this
36
+ control. Empty lists, incompatible operators, and selections outside the list
37
+ raise an error.
38
+
39
+ Use `defaultOperator` for an uncontrolled picker, or `operator` together with
40
+ `onOperatorChange(event, operator)` for a controlled picker. The initial operator
41
+ is `=` if allowed, otherwise the first option. Operator control is independent
42
+ of value control (`value` / `defaultValue`).
43
+
44
+ Selecting a different operator preserves the value and immediately commits a
45
+ populated, valid filter using the new operator. An already empty value only changes the
46
+ selection. Switching to a range retains the lower value and starts with an empty
47
+ upper value; switching back retains the lower value (and clears a filter if only
48
+ the upper value was populated). Incomplete ranges use the
49
+ existing partial-range commit behaviour. The operator picker respects
50
+ `InputProps.disabled` and `InputProps.readOnly`.
51
+
52
+ `FilterContainerColumnFilter` accepts the same picker options, manages selection
53
+ internally, and restores operators and values from externally loaded or saved
54
+ filters. A loaded operator must be included in the configured options. Clearing
55
+ an external filter resets the operator to its default; clearing a value locally
56
+ keeps the selected operator.
57
+
18
58
  onCommit is a `ColumnFilterCommitHandler`
19
59
 
20
60
  ```typescript
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.5.14",
2
+ "version": "3.5.16",
3
3
  "main": "./src/index.js",
4
4
  "author": "heswell",
5
5
  "license": "Apache-2.0",
@@ -10,19 +10,19 @@
10
10
  "type-defs": "node ../../scripts/build-type-defs.mjs"
11
11
  },
12
12
  "devDependencies": {
13
- "@vuu-ui/vuu-data-types": "3.5.14",
14
- "@vuu-ui/vuu-protocol-types": "3.5.14",
15
- "@vuu-ui/vuu-table-types": "3.5.14",
16
- "@vuu-ui/vuu-filter-types": "3.5.14"
13
+ "@vuu-ui/vuu-data-types": "3.5.16",
14
+ "@vuu-ui/vuu-protocol-types": "3.5.16",
15
+ "@vuu-ui/vuu-table-types": "3.5.16",
16
+ "@vuu-ui/vuu-filter-types": "3.5.16"
17
17
  },
18
18
  "dependencies": {
19
- "@vuu-ui/vuu-context-menu": "3.5.14",
20
- "@vuu-ui/vuu-data-react": "3.5.14",
21
- "@vuu-ui/vuu-filter-parser": "3.5.14",
22
- "@vuu-ui/vuu-popups": "3.5.14",
23
- "@vuu-ui/vuu-ui-controls": "3.5.14",
24
- "@vuu-ui/vuu-table": "3.5.14",
25
- "@vuu-ui/vuu-utils": "3.5.14",
19
+ "@vuu-ui/vuu-context-menu": "3.5.16",
20
+ "@vuu-ui/vuu-data-react": "3.5.16",
21
+ "@vuu-ui/vuu-filter-parser": "3.5.16",
22
+ "@vuu-ui/vuu-popups": "3.5.16",
23
+ "@vuu-ui/vuu-ui-controls": "3.5.16",
24
+ "@vuu-ui/vuu-table": "3.5.16",
25
+ "@vuu-ui/vuu-utils": "3.5.16",
26
26
  "@salt-ds/core": "1.54.1",
27
27
  "@salt-ds/lab": "1.0.0-alpha.83",
28
28
  "@salt-ds/styles": "0.2.1",
@@ -1,4 +1,4 @@
1
- import { EpochTimestamp, getTemporalInfo, getTypedValueForDescriptor, isAndFilter, isBetweenFilter, isExtendedFilter, isMultiClauseFilter, isSingleValueFilter } from "@vuu-ui/vuu-utils";
1
+ import { EpochTimestamp, getTemporalInfo, getTypedValueForDescriptor, isAndFilter, isBetweenFilter, isBetweenOperator, isExtendedFilter, isMultiClauseFilter, isSingleValueFilter } from "@vuu-ui/vuu-utils";
2
2
  import { ExtendedSingleValueFilterClause } from "./filter-container/ExtendedSingleValueFilterClause.js";
3
3
  function installExtendedFilters(filter, throwIfUndefined = false) {
4
4
  if (void 0 !== filter) if (isExtendedFilter(filter)) {
@@ -52,7 +52,11 @@ const singleValueOps = [
52
52
  "starts",
53
53
  "ends"
54
54
  ];
55
- const asSingleValueOp = (op)=>singleValueOps.includes(op) ? op : "=";
55
+ const asSingleValueOp = (op)=>{
56
+ const operator = singleValueOps.find((candidate)=>candidate === op);
57
+ if (void 0 === operator) throw Error(`[FilterAggregator] unsupported single-value operator ${op}`);
58
+ return operator;
59
+ };
56
60
  class FilterAggregator {
57
61
  #filters = new Map();
58
62
  constructor(filter){
@@ -87,7 +91,7 @@ class FilterAggregator {
87
91
  } else {
88
92
  const typedValue = getFilterValue(column, value.toString(), temporalInfo, options, true);
89
93
  if (void 0 === typedValue) this.#filters.delete(column.name);
90
- else this.#filters.set(column.name, createClause(temporalInfo ? asSingleValueOp(op) : "=", typedValue));
94
+ else this.#filters.set(column.name, createClause(isBetweenOperator(op) ? "=" : asSingleValueOp(op), typedValue));
91
95
  }
92
96
  }
93
97
  has({ name }) {
@@ -51,6 +51,29 @@ const css = `
51
51
  border-color: red;
52
52
  }
53
53
 
54
+ .vuuColumnFilter.vuuColumnFilter-withOperator {
55
+ max-width: none;
56
+
57
+ & [data-field="operator"] {
58
+ border-right: 1px solid var(--salt-separable-primary-borderColor);
59
+ margin-right: var(--salt-spacing-100);
60
+ align-self: stretch;
61
+ display: flex;
62
+ }
63
+
64
+ & .vuuColumnFilter-operator {
65
+ background: none;
66
+ border: none;
67
+ width: auto;
68
+ min-width: 60px;
69
+
70
+ &.saltDropdown .saltDropdown-toggle {
71
+ flex-shrink: 0;
72
+ display: block;
73
+ }
74
+ }
75
+ }
76
+
54
77
 
55
78
  `;
56
79
  export default css;
@@ -1,15 +1,18 @@
1
- import { jsxs } from "react/jsx-runtime";
2
- import { SegmentedButtonGroup } from "@salt-ds/core";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { SegmentedButtonGroup, useForkRef } from "@salt-ds/core";
3
3
  import { getDataItemEditControl } from "@vuu-ui/vuu-data-react";
4
4
  import clsx from "clsx";
5
- import { forwardRef as external_react_forwardRef, useMemo } from "react";
5
+ import { forwardRef as external_react_forwardRef, useMemo, useRef } from "react";
6
6
  import { useColumnFilter } from "./useColumnFilter.js";
7
7
  import { useComponentCssInjection } from "@salt-ds/styles";
8
8
  import { useWindow } from "@salt-ds/window";
9
9
  import ColumnFilter_0 from "./ColumnFilter.css";
10
10
  import { isBetweenOperator, withDateTimePattern } from "@vuu-ui/vuu-utils";
11
+ import { OperatorPicker } from "../filter-clause/operator-picker/OperatorPicker.js";
11
12
  const classBase = "vuuColumnFilter";
12
- const ColumnFilter_ColumnFilter = /*#__PURE__*/ external_react_forwardRef(function({ InputProps: InputPropsProp, TypeaheadProps, className, column, defaultValue, extendedFilterOptions, labels, onColumnFilterChange, onColumnRangeFilterChange, onCommit: onCommitProp, operator = "=", pattern, table, value: valueProp, values, variant, ...buttonGroupProps }, forwardRef) {
13
+ const ColumnFilter_ColumnFilter = /*#__PURE__*/ external_react_forwardRef(function({ InputProps: InputPropsProp, TypeaheadProps, className, column, defaultValue, defaultOperator, extendedFilterOptions, labels, onColumnFilterChange, onColumnRangeFilterChange, onCommit: onCommitProp, onOperatorChange, operator: operatorProp, operators: operatorsProp, showOperatorPicker = false, pattern, table, value: valueProp, values, variant, ...buttonGroupProps }, forwardRef) {
14
+ const rootRef = useRef(null);
15
+ const ref = useForkRef(rootRef, forwardRef);
13
16
  const targetWindow = useWindow();
14
17
  useComponentCssInjection({
15
18
  testId: "vuu-filter-editor",
@@ -25,25 +28,45 @@ const ColumnFilter_ColumnFilter = /*#__PURE__*/ external_react_forwardRef(functi
25
28
  date,
26
29
  time
27
30
  ]);
28
- const { InputProps, InputPropsRange, isInvalid, onCommit, onCommitRange } = useColumnFilter({
31
+ const { InputProps, InputPropsRange, isInvalid, onCommit, onCommitRange, operator, operators, onOperatorSelect } = useColumnFilter({
29
32
  InputProps: InputPropsProp,
30
33
  column: controlDescriptor,
31
34
  defaultValue,
35
+ defaultOperator,
32
36
  extendedFilterOptions,
33
37
  onColumnFilterChange,
34
38
  onColumnRangeFilterChange,
35
39
  onCommit: onCommitProp,
36
- operator,
40
+ onOperatorChange,
41
+ operator: operatorProp,
42
+ operators: operatorsProp,
43
+ showOperatorPicker,
37
44
  value: valueProp
38
45
  });
39
46
  return /*#__PURE__*/ jsxs(SegmentedButtonGroup, {
40
47
  ...buttonGroupProps,
41
48
  className: clsx(classBase, className, {
42
49
  [`${classBase}-invalid`]: isInvalid,
43
- [`${classBase}-toggle`]: "toggle" === variant
50
+ [`${classBase}-toggle`]: "toggle" === variant,
51
+ [`${classBase}-withOperator`]: showOperatorPicker
44
52
  }),
45
- ref: forwardRef,
53
+ ref: ref,
46
54
  children: [
55
+ operators ? /*#__PURE__*/ jsx(OperatorPicker, {
56
+ "aria-label": `${column.label ?? column.name} operator`,
57
+ className: `${classBase}-operator`,
58
+ disabled: InputProps?.disabled,
59
+ readOnly: InputProps?.readOnly,
60
+ operators: operators,
61
+ onSelect: (event, operator)=>{
62
+ onOperatorSelect(event, operator);
63
+ requestAnimationFrame(()=>{
64
+ rootRef.current?.querySelector("input:not(:disabled)")?.focus();
65
+ });
66
+ },
67
+ value: operator,
68
+ variant: "dropdown"
69
+ }) : null,
47
70
  getDataItemEditControl({
48
71
  InputProps,
49
72
  TypeaheadProps,
@@ -0,0 +1,40 @@
1
+ import { extractFilterForColumn, isBetweenFilter, isBetweenOperator, isNumericColumn, isSingleValueFilter, isTemporalColumn, isTextColumn } from "@vuu-ui/vuu-utils";
2
+ import { numericOperators, textOperators } from "../filter-clause/operator-picker/operator-utils.js";
3
+ const getColumnFilterOperators = (column, operators)=>{
4
+ let supported;
5
+ if (isTextColumn(column)) supported = textOperators.filter((op)=>"in" !== op);
6
+ else if (isNumericColumn(column) || isTemporalColumn(column) || column.serverDataType?.startsWith("scaleddecimal")) supported = [
7
+ ...numericOperators,
8
+ "between",
9
+ "between-inclusive"
10
+ ];
11
+ else if ("boolean" === column.serverDataType) supported = [
12
+ "=",
13
+ "!="
14
+ ];
15
+ else throw Error(`[ColumnFilter] unsupported column type for ${column.name}`);
16
+ if (operators) {
17
+ if (0 === operators.length || operators.some((op)=>!supported.includes(op))) throw Error(`[ColumnFilter] invalid operators for ${column.name}`);
18
+ return operators;
19
+ }
20
+ return supported.filter((op)=>!isBetweenOperator(op));
21
+ };
22
+ const getDefaultColumnFilterOperator = (operators, operator)=>{
23
+ const selected = void 0 === operator ? operators.find((op)=>"=" === op) ?? operators[0] : operators.find((op)=>op === operator);
24
+ if (void 0 === selected) throw Error(`[ColumnFilter] operator ${operator} is not in the configured operators`);
25
+ return selected;
26
+ };
27
+ const getColumnFilterOperator = (column, filter)=>{
28
+ const clause = getColumnFilterClause(column, filter);
29
+ if (isBetweenFilter(clause)) return ">=" === clause.filters[0].op ? "between-inclusive" : "between";
30
+ if (isSingleValueFilter(clause)) return clause.op;
31
+ };
32
+ const getColumnFilterClause = (column, filter)=>{
33
+ const clause = extractFilterForColumn(filter, column.name);
34
+ return isSingleValueFilter(clause) || isBetweenFilter(clause) ? clause : void 0;
35
+ };
36
+ const normalizeColumnFilterValue = (operator, value = "")=>isBetweenOperator(operator) ? Array.isArray(value) ? value : [
37
+ `${value}`,
38
+ ""
39
+ ] : Array.isArray(value) ? value[0] : value;
40
+ export { getColumnFilterClause, getColumnFilterOperator, getColumnFilterOperators, getDefaultColumnFilterOperator, normalizeColumnFilterValue };
@@ -1,6 +1,7 @@
1
1
  import { useControlled } from "@salt-ds/core";
2
2
  import { useCallback, useMemo, useState } from "react";
3
3
  import { isBetweenOperator, isValidRange } from "@vuu-ui/vuu-utils";
4
+ import { getColumnFilterOperators, getDefaultColumnFilterOperator, normalizeColumnFilterValue } from "./column-filter-utils.js";
4
5
  const injectInputProps = (InputProps, inputProps)=>{
5
6
  if (void 0 === InputProps) return {
6
7
  inputProps
@@ -13,20 +14,59 @@ const injectInputProps = (InputProps, inputProps)=>{
13
14
  }
14
15
  };
15
16
  };
16
- const useColumnFilter = ({ InputProps: InputPropsProp, column, defaultValue, extendedFilterOptions, onColumnFilterChange, onColumnRangeFilterChange, onCommit, operator = "=", value: valueProp })=>{
17
+ const useColumnFilter = ({ InputProps: InputPropsProp, column, defaultValue, defaultOperator, extendedFilterOptions, onColumnFilterChange, onColumnRangeFilterChange, onCommit, onOperatorChange, operator: operatorProp, operators: operatorsProp, showOperatorPicker = false, value: valueProp })=>{
17
18
  const [isInvalid, setIsInvalid] = useState(false);
18
- const [value, setValue] = useControlled({
19
+ const operators = useMemo(()=>showOperatorPicker ? getColumnFilterOperators(column, operatorsProp) : void 0, [
20
+ column,
21
+ operatorsProp,
22
+ showOperatorPicker
23
+ ]);
24
+ const [selectedOperator, setOperator] = useControlled({
25
+ controlled: operatorProp,
26
+ default: operators ? getDefaultColumnFilterOperator(operators, defaultOperator) : defaultOperator ?? "=",
27
+ name: "ColumnFilter",
28
+ state: "operator"
29
+ });
30
+ const operator = operators ? getDefaultColumnFilterOperator(operators, selectedOperator) : selectedOperator;
31
+ const [rawValue, setValue] = useControlled({
19
32
  controlled: valueProp,
20
33
  default: defaultValue,
21
- name: "ColumnFilterNext",
34
+ name: "ColumnFilter",
22
35
  state: "value"
23
36
  });
37
+ const value = showOperatorPicker ? normalizeColumnFilterValue(operator, rawValue) : rawValue;
38
+ const handleOperatorSelect = useCallback((event, newOperator)=>{
39
+ if (newOperator === operator) return;
40
+ const newValue = normalizeColumnFilterValue(newOperator, value);
41
+ setOperator(newOperator);
42
+ setValue(newValue);
43
+ setIsInvalid(false);
44
+ onOperatorChange?.(event, newOperator);
45
+ const hadValue = Array.isArray(value) ? value.some((v)=>"" !== v) : void 0 !== value && "" !== value;
46
+ if (hadValue) {
47
+ const valid = !Array.isArray(newValue) || isValidRange(newValue);
48
+ setIsInvalid(!valid);
49
+ if (valid) onCommit(column, newOperator, newValue, extendedFilterOptions);
50
+ }
51
+ }, [
52
+ column,
53
+ extendedFilterOptions,
54
+ onCommit,
55
+ onOperatorChange,
56
+ operator,
57
+ setOperator,
58
+ setValue,
59
+ value
60
+ ]);
24
61
  const handleCommit = useCallback((_e, newValue = "")=>{
25
62
  if (Array.isArray(value)) if (isBetweenOperator(operator)) {
26
- setValue([
63
+ const newRange = [
27
64
  `${newValue}`,
28
65
  value[1]
29
- ]);
66
+ ];
67
+ setValue(newRange);
68
+ if (showOperatorPicker && !isValidRange(newRange)) return void setIsInvalid(true);
69
+ setIsInvalid(false);
30
70
  onCommit?.(column, operator, [
31
71
  `${newValue}`,
32
72
  value[1]
@@ -42,7 +82,8 @@ const useColumnFilter = ({ InputProps: InputPropsProp, column, defaultValue, ext
42
82
  setValue,
43
83
  onCommit,
44
84
  column,
45
- extendedFilterOptions
85
+ extendedFilterOptions,
86
+ showOperatorPicker
46
87
  ]);
47
88
  const handleRangeCommit = useCallback((_e, newValue = "")=>{
48
89
  if (Array.isArray(value)) {
@@ -79,29 +120,37 @@ const useColumnFilter = ({ InputProps: InputPropsProp, column, defaultValue, ext
79
120
  ]);
80
121
  const onChange = useCallback((e)=>{
81
122
  const { value = "" } = e.target;
82
- setValue((v)=>Array.isArray(v) ? [
123
+ setValue((v)=>{
124
+ const current = showOperatorPicker ? normalizeColumnFilterValue(operator, v) : v;
125
+ return Array.isArray(current) ? [
83
126
  value,
84
- v[1]
85
- ] : value);
127
+ current[1]
128
+ ] : value;
129
+ });
86
130
  onColumnFilterChange?.(e.target.value, column, operator);
87
131
  }, [
88
132
  column,
89
133
  onColumnFilterChange,
90
134
  operator,
91
- setValue
135
+ setValue,
136
+ showOperatorPicker
92
137
  ]);
93
138
  const onRangeInputChange = useCallback((e)=>{
94
139
  const { value = "" } = e.target;
95
- setValue((v)=>Array.isArray(v) ? [
96
- v[0],
140
+ setValue((v)=>{
141
+ const current = showOperatorPicker ? normalizeColumnFilterValue(operator, v) : v;
142
+ return Array.isArray(current) ? [
143
+ current[0],
97
144
  value
98
- ] : value);
145
+ ] : value;
146
+ });
99
147
  onColumnRangeFilterChange?.(value, column, operator);
100
148
  }, [
101
149
  setValue,
102
150
  onColumnRangeFilterChange,
103
151
  column,
104
- operator
152
+ operator,
153
+ showOperatorPicker
105
154
  ]);
106
155
  const InputProps = useMemo(()=>injectInputProps(InputPropsProp, {
107
156
  onChange,
@@ -122,9 +171,12 @@ const useColumnFilter = ({ InputProps: InputPropsProp, column, defaultValue, ext
122
171
  return {
123
172
  InputProps,
124
173
  InputPropsRange,
125
- isInvalid,
174
+ isInvalid: showOperatorPicker ? Array.isArray(value) && !isValidRange(value) : isInvalid,
126
175
  onCommit: handleCommit,
127
- onCommitRange: handleRangeCommit
176
+ onCommitRange: handleRangeCommit,
177
+ operator,
178
+ operators,
179
+ onOperatorSelect: handleOperatorSelect
128
180
  };
129
181
  };
130
182
  export { useColumnFilter };
@@ -1,10 +1,37 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { Option, useForkRef } from "@salt-ds/core";
2
+ import { Dropdown, Option, useForkRef } from "@salt-ds/core";
3
3
  import { forwardRef } from "react";
4
4
  import { ExpandoCombobox } from "../ExpandoCombobox.js";
5
5
  import { getOperators } from "./operator-utils.js";
6
6
  import { useOperatorPicker } from "./useOperatorPicker.js";
7
- const OperatorPicker_OperatorPicker = /*#__PURE__*/ forwardRef(function({ className, column, dropdownOnAutofocus = true, inputProps, onSelect, value: valueProp }, forwardedRef) {
7
+ const OperatorPicker_OperatorPicker = /*#__PURE__*/ forwardRef(function(props, forwardedRef) {
8
+ if ("dropdown" === props.variant) {
9
+ const { operators, value, onSelect, variant: _variant, ...dropdownProps } = props;
10
+ return /*#__PURE__*/ jsx("div", {
11
+ ref: forwardedRef,
12
+ "data-field": "operator",
13
+ children: /*#__PURE__*/ jsx(Dropdown, {
14
+ ...dropdownProps,
15
+ value: value,
16
+ selected: [
17
+ value
18
+ ],
19
+ onSelectionChange: (evt, [operator])=>{
20
+ if (void 0 !== operator) onSelect(evt, operator);
21
+ },
22
+ children: operators.map((op)=>/*#__PURE__*/ jsx(Option, {
23
+ value: op,
24
+ children: op
25
+ }, op))
26
+ })
27
+ });
28
+ }
29
+ return /*#__PURE__*/ jsx(OperatorPicker_OperatorCombobox, {
30
+ ...props,
31
+ ref: forwardedRef
32
+ });
33
+ });
34
+ const OperatorPicker_OperatorCombobox = /*#__PURE__*/ forwardRef(function({ className, column, dropdownOnAutofocus = true, inputProps, onSelect, operators, value: valueProp }, forwardedRef) {
8
35
  const { ref, ...comboProps } = useOperatorPicker({
9
36
  onSelect,
10
37
  value: valueProp?.toString() ?? ""
@@ -17,7 +44,7 @@ const OperatorPicker_OperatorPicker = /*#__PURE__*/ forwardRef(function({ classN
17
44
  inputProps: inputProps,
18
45
  ref: useForkRef(forwardedRef, ref),
19
46
  title: "operator",
20
- children: getOperators(column).filter((op)=>op.toLowerCase().includes(comboProps.value.toLowerCase())).map((op)=>/*#__PURE__*/ jsx(Option, {
47
+ children: (operators ?? getOperators(column)).filter((op)=>op.toLowerCase().includes(comboProps.value.toLowerCase())).map((op)=>/*#__PURE__*/ jsx(Option, {
21
48
  value: op
22
49
  }, op))
23
50
  });
@@ -3,14 +3,16 @@ import clsx from "clsx";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { ColumnFilterContext, useFilterContainer, useFilterContext } from "./useFilterContainer.js";
5
5
  import { ColumnFilter } from "../column-filter/ColumnFilter.js";
6
- import { filterDescriptorHasFilter, isNullFilter, useColumnFilterRegistry, useSavedFilters } from "../filter-provider/FilterContext.js";
6
+ import { UNSAVED_FILTER, filterDescriptorHasFilter, isNullFilter, useColumnFilterRegistry, useSavedFilters } from "../filter-provider/FilterContext.js";
7
7
  import { filtersAreEqual, getColumnValueFromFilter, getDefaultTimeRange, isBetweenOperator, isTimeDataValue, withDateTimePattern } from "@vuu-ui/vuu-utils";
8
+ import { useControlled } from "@salt-ds/core";
9
+ import { getColumnFilterClause, getColumnFilterOperator, getColumnFilterOperators, getDefaultColumnFilterOperator, normalizeColumnFilterValue } from "../column-filter/column-filter-utils.js";
8
10
  import { useComponentCssInjection } from "@salt-ds/styles";
9
11
  import { useWindow } from "@salt-ds/window";
10
12
  import FilterContainer from "./FilterContainer.css";
11
13
  const classBase = "vuuFilterContainer";
12
14
  const notEmpty = (value)=>Array.isArray(value) ? "" !== value[0] && "" !== value[1] : "" !== value;
13
- const FilterContainerColumnFilter = ({ column: columnProp, operator = "=", pattern: patternProp, variant, ...props })=>{
15
+ const FilterContainerColumnFilter = ({ column: columnProp, operator: operatorProp, defaultOperator, showOperatorPicker = false, operators, onOperatorChange, pattern: patternProp, variant, ...props })=>{
14
16
  const { getColumnFilterPattern } = useColumnFilterRegistry();
15
17
  const pattern = patternProp ?? getColumnFilterPattern(columnProp);
16
18
  const { date, time } = pattern ?? {};
@@ -22,7 +24,20 @@ const FilterContainerColumnFilter = ({ column: columnProp, operator = "=", patte
22
24
  date,
23
25
  time
24
26
  ]);
25
- const { filterProviderKey, onChange: onFilterContextChange, onCommit: onFilterContextCommit, register } = useFilterContext(column, true);
27
+ const { filter: containerFilter, getFilter, filterProviderKey, onChange: onFilterContextChange, onCommit: onFilterContextCommit, register } = useFilterContext(column, true);
28
+ const availableOperators = useMemo(()=>showOperatorPicker ? getColumnFilterOperators(column, operators) : void 0, [
29
+ column,
30
+ operators,
31
+ showOperatorPicker
32
+ ]);
33
+ const [initialOperator] = useState(()=>availableOperators ? getDefaultColumnFilterOperator(availableOperators, operatorProp ?? getColumnFilterOperator(column, containerFilter) ?? defaultOperator) : defaultOperator ?? "=");
34
+ const [selectedOperator, setOperator] = useControlled({
35
+ controlled: operatorProp,
36
+ default: initialOperator,
37
+ name: "FilterContainerColumnFilter",
38
+ state: "operator"
39
+ });
40
+ const operator = selectedOperator;
26
41
  const { registerColumnFilterColumn } = useColumnFilterRegistry(filterProviderKey);
27
42
  useEffect(()=>registerColumnFilterColumn(column), [
28
43
  column,
@@ -35,10 +50,50 @@ const FilterContainerColumnFilter = ({ column: columnProp, operator = "=", patte
35
50
  ]);
36
51
  const [value, setValue] = useState(initialValue);
37
52
  const valueRef = useRef(initialValue);
53
+ if (showOperatorPicker) {
54
+ const normalizedValue = normalizeColumnFilterValue(operator, value);
55
+ if (normalizedValue !== value) {
56
+ valueRef.current = normalizedValue;
57
+ setValue(normalizedValue);
58
+ }
59
+ }
38
60
  const { currentFilter } = useSavedFilters(filterProviderKey);
39
61
  const currentFilterRef = useRef(currentFilter);
62
+ const containerFilterRef = useRef(containerFilter);
63
+ const localFilterRef = useRef(containerFilter);
64
+ useMemo(()=>{
65
+ if (!showOperatorPicker) return;
66
+ const containerChanged = !filtersAreEqual(containerFilterRef.current, containerFilter);
67
+ const savedChanged = currentFilterRef.current.id !== currentFilter.id || !filtersAreEqual(currentFilterRef.current.filter, currentFilter.filter);
68
+ if (containerChanged || savedChanged) {
69
+ const savedIdChanged = currentFilterRef.current.id !== currentFilter.id;
70
+ const filter = containerChanged ? containerFilter : currentFilter.filter ?? void 0;
71
+ containerFilterRef.current = containerFilter;
72
+ currentFilterRef.current = currentFilter;
73
+ const savedFilterActivated = savedIdChanged && filterDescriptorHasFilter(currentFilter) && currentFilter.id !== UNSAVED_FILTER;
74
+ const clause = getColumnFilterClause(column, filter);
75
+ if (savedFilterActivated || !filtersAreEqual(clause, getColumnFilterClause(column, localFilterRef.current))) {
76
+ const nextOperator = operatorProp ?? (availableOperators ? getDefaultColumnFilterOperator(availableOperators, getColumnFilterOperator(column, filter) ?? defaultOperator) : operator);
77
+ const nextValue = normalizeColumnFilterValue(nextOperator, getColumnValueFromFilter(column, nextOperator, clause));
78
+ setOperator(nextOperator);
79
+ valueRef.current = nextValue;
80
+ setValue(nextValue);
81
+ localFilterRef.current = filter;
82
+ }
83
+ }
84
+ }, [
85
+ availableOperators,
86
+ column,
87
+ containerFilter,
88
+ currentFilter,
89
+ defaultOperator,
90
+ operator,
91
+ operatorProp,
92
+ setOperator,
93
+ showOperatorPicker
94
+ ]);
40
95
  useMemo(()=>{
41
- if (currentFilterRef.current.id !== currentFilter.id || !filtersAreEqual(currentFilterRef.current.filter, currentFilter.filter)) {
96
+ if (!showOperatorPicker && (currentFilterRef.current.id !== currentFilter.id || !filtersAreEqual(currentFilterRef.current.filter, currentFilter.filter))) {
42
97
  currentFilterRef.current = currentFilter;
43
98
  if (isNullFilter(currentFilter) && notEmpty(valueRef.current)) {
44
99
  if (Array.isArray(valueRef.current)) if (isTimeDataValue(column)) valueRef.current = getDefaultTimeRange(column);
@@ -66,13 +121,16 @@ const FilterContainerColumnFilter = ({ column: columnProp, operator = "=", patte
66
121
  }
67
122
  }, [
68
123
  column,
69
- currentFilter
124
+ currentFilter,
125
+ showOperatorPicker
70
126
  ]);
71
127
  const handleCommit = useCallback((column, op, value, extendedFilterOptions)=>{
72
128
  valueRef.current = value;
73
129
  setValue(value);
74
130
  onFilterContextCommit(column, op, value, extendedFilterOptions);
131
+ localFilterRef.current = getFilter();
75
132
  }, [
133
+ getFilter,
76
134
  onFilterContextCommit
77
135
  ]);
78
136
  const handleColumnFilterChange = useCallback((value, column, op)=>{
@@ -83,7 +141,9 @@ const FilterContainerColumnFilter = ({ column: columnProp, operator = "=", patte
83
141
  ] : value;
84
142
  setValue(valueRef.current);
85
143
  onFilterContextChange(valueRef.current, column, op);
144
+ localFilterRef.current = getFilter();
86
145
  }, [
146
+ getFilter,
87
147
  onFilterContextChange
88
148
  ]);
89
149
  const handleColumnRangeFilterChange = useCallback((value, column, op)=>{
@@ -94,7 +154,9 @@ const FilterContainerColumnFilter = ({ column: columnProp, operator = "=", patte
94
154
  ] : value;
95
155
  setValue(valueRef.current);
96
156
  onFilterContextChange(valueRef.current, column, op);
157
+ localFilterRef.current = getFilter();
97
158
  }, [
159
+ getFilter,
98
160
  onFilterContextChange
99
161
  ]);
100
162
  return /*#__PURE__*/ jsx(ColumnFilter, {
@@ -104,6 +166,14 @@ const FilterContainerColumnFilter = ({ column: columnProp, operator = "=", patte
104
166
  onColumnRangeFilterChange: handleColumnRangeFilterChange,
105
167
  onCommit: handleCommit,
106
168
  operator: operator,
169
+ onOperatorChange: (event, op)=>{
170
+ setOperator(op);
171
+ valueRef.current = normalizeColumnFilterValue(op, valueRef.current);
172
+ setValue(valueRef.current);
173
+ onOperatorChange?.(event, op);
174
+ },
175
+ operators: operators,
176
+ showOperatorPicker: showOperatorPicker,
107
177
  value: value,
108
178
  variant: variant
109
179
  });
@@ -7,6 +7,8 @@ const ColumnFilterContext = createContext({
7
7
  function useFilterContext(column, throwIfNoContainer = false) {
8
8
  const ctx = useContext(ColumnFilterContext);
9
9
  if (ctx) return {
10
+ filter: ctx.filter,
11
+ getFilter: ctx.getFilter,
10
12
  filterProviderKey: ctx.filterProviderKey,
11
13
  register: ctx.register,
12
14
  getValue: ctx.getValue,
@@ -29,6 +31,9 @@ const useFilterContainer = ({ filter, filterProviderKey = "GLOBAL", onFilterAppl
29
31
  const filterAggregator = useMemo(()=>new FilterAggregator(filter), [
30
32
  filter
31
33
  ]);
34
+ const getFilter = useCallback(()=>filterAggregator.filter, [
35
+ filterAggregator
36
+ ]);
32
37
  const register = useCallback((column, op)=>valueRef.current[column.name] = getColumnValueFromFilter(column, op, filterRef.current), []);
33
38
  const getValue = useCallback((column, fallbackValue)=>{
34
39
  const value = valueRef.current[column.name];
@@ -72,6 +77,8 @@ const useFilterContainer = ({ filter, filterProviderKey = "GLOBAL", onFilterAppl
72
77
  ]);
73
78
  return {
74
79
  filterContainerInstalled: true,
80
+ filter,
81
+ getFilter,
75
82
  filterProviderKey,
76
83
  onChange: handleInputChange,
77
84
  onCommit: handleCommit,
@@ -0,0 +1,8 @@
1
+ import type { ColumnFilterOp, ColumnFilterValue, FilterContainerFilter } from "@vuu-ui/vuu-filter-types";
2
+ import type { ColumnDescriptor } from "@vuu-ui/vuu-table-types";
3
+ export type SelectableColumnFilterOp = Exclude<ColumnFilterOp, "in">;
4
+ export declare const getColumnFilterOperators: (column: ColumnDescriptor, operators?: readonly SelectableColumnFilterOp[]) => readonly SelectableColumnFilterOp[];
5
+ export declare const getDefaultColumnFilterOperator: (operators: readonly SelectableColumnFilterOp[], operator?: ColumnFilterOp) => SelectableColumnFilterOp;
6
+ export declare const getColumnFilterOperator: (column: ColumnDescriptor, filter?: FilterContainerFilter) => SelectableColumnFilterOp | undefined;
7
+ export declare const getColumnFilterClause: (column: ColumnDescriptor, filter?: FilterContainerFilter) => FilterContainerFilter | undefined;
8
+ export declare const normalizeColumnFilterValue: (operator: ColumnFilterOp, value?: ColumnFilterValue) => ColumnFilterValue;
@@ -1,8 +1,10 @@
1
1
  import { ColumnFilterChangeHandler, ColumnFilterCommitHandler, ColumnFilterOp, ColumnFilterValue, ExtendedFilterOptions } from "@vuu-ui/vuu-filter-types";
2
2
  import { ColumnDescriptor } from "@vuu-ui/vuu-table-types";
3
3
  import { InputProps } from "@salt-ds/core";
4
+ import { SyntheticEvent } from "react";
4
5
  import { CommitHandler } from "@vuu-ui/vuu-utils";
5
6
  import { DataItemEditControlProps } from "@vuu-ui/vuu-data-react";
7
+ import { type SelectableColumnFilterOp } from "./column-filter-utils";
6
8
  export interface ColumnFilterHookProps extends Pick<DataItemEditControlProps, "InputProps"> {
7
9
  column: ColumnDescriptor;
8
10
  /**
@@ -27,17 +29,29 @@ export interface ColumnFilterHookProps extends Pick<DataItemEditControlProps, "I
27
29
  * tabbing away from control or making selection from list
28
30
  */
29
31
  onCommit: ColumnFilterCommitHandler;
32
+ /** Controlled operator. Defaults to '=' when the picker is hidden. */
30
33
  operator?: ColumnFilterOp;
34
+ /** Initial operator for an uncontrolled picker. Defaults to '=' or the first option. */
35
+ defaultOperator?: ColumnFilterOp;
36
+ /** Render an operator dropdown before the value. Defaults to false. */
37
+ showOperatorPicker?: boolean;
38
+ /** Ordered operator options, replacing the column-type defaults. 'in' is not supported. */
39
+ operators?: readonly SelectableColumnFilterOp[];
40
+ /** Called before committing the current value with the newly selected operator. */
41
+ onOperatorChange?: (event: SyntheticEvent, operator: ColumnFilterOp) => void;
31
42
  /**
32
43
  * Filter value. Pair of values expected when operator is
33
44
  * 'between'. If provided, component is controlled.
34
45
  */
35
46
  value?: ColumnFilterValue;
36
47
  }
37
- export declare const useColumnFilter: ({ InputProps: InputPropsProp, column, defaultValue, extendedFilterOptions, onColumnFilterChange, onColumnRangeFilterChange, onCommit, operator, value: valueProp, }: ColumnFilterHookProps) => {
48
+ export declare const useColumnFilter: ({ InputProps: InputPropsProp, column, defaultValue, defaultOperator, extendedFilterOptions, onColumnFilterChange, onColumnRangeFilterChange, onCommit, onOperatorChange, operator: operatorProp, operators: operatorsProp, showOperatorPicker, value: valueProp, }: ColumnFilterHookProps) => {
38
49
  InputProps: InputProps;
39
50
  InputPropsRange: InputProps;
40
51
  isInvalid: boolean;
41
52
  onCommit: CommitHandler<HTMLElement>;
42
53
  onCommitRange: CommitHandler<HTMLElement>;
54
+ operator: "in" | SelectableColumnFilterOp;
55
+ operators: readonly SelectableColumnFilterOp[] | undefined;
56
+ onOperatorSelect: (event: SyntheticEvent, newOperator: ColumnFilterOp) => void;
43
57
  };
@@ -1,14 +1,24 @@
1
- import type { FilterClauseOp } from "@vuu-ui/vuu-filter-types";
1
+ import type { ColumnFilterOp, FilterClauseOp } from "@vuu-ui/vuu-filter-types";
2
2
  import type { ColumnDescriptor } from "@vuu-ui/vuu-table-types";
3
3
  import { ComboBoxProps } from "@salt-ds/core";
4
4
  import { SyntheticEvent } from "react";
5
- export type OperatorPickerProps = Pick<ComboBoxProps, "className" | "inputProps" | "value"> & {
5
+ type OperatorComboboxProps = Pick<ComboBoxProps, "className" | "inputProps" | "value"> & {
6
6
  column: ColumnDescriptor;
7
7
  onSelect: (evt: SyntheticEvent, operator: FilterClauseOp) => void;
8
8
  dropdownOnAutofocus?: boolean;
9
+ variant?: "combobox";
10
+ operators?: readonly FilterClauseOp[];
9
11
  };
10
- export declare const OperatorPicker: import("react").ForwardRefExoticComponent<Pick<ComboBoxProps, "value" | "className" | "inputProps"> & {
11
- column: ColumnDescriptor;
12
- onSelect: (evt: SyntheticEvent, operator: FilterClauseOp) => void;
13
- dropdownOnAutofocus?: boolean;
14
- } & import("react").RefAttributes<HTMLDivElement>>;
12
+ type OperatorDropdownProps = {
13
+ variant: "dropdown";
14
+ className?: string;
15
+ disabled?: boolean;
16
+ readOnly?: boolean;
17
+ operators: readonly ColumnFilterOp[];
18
+ value: ColumnFilterOp;
19
+ onSelect: (evt: SyntheticEvent, operator: ColumnFilterOp) => void;
20
+ "aria-label"?: string;
21
+ };
22
+ export type OperatorPickerProps = OperatorComboboxProps | OperatorDropdownProps;
23
+ export declare const OperatorPicker: import("react").ForwardRefExoticComponent<OperatorPickerProps & import("react").RefAttributes<HTMLDivElement>>;
24
+ export {};
@@ -16,7 +16,7 @@ export interface FilterContainerColumnFilterProps extends Omit<ColumnFilterProps
16
16
  */
17
17
  pattern?: DateTimePattern;
18
18
  }
19
- export declare const FilterContainerColumnFilter: ({ column: columnProp, operator, pattern: patternProp, variant, ...props }: FilterContainerColumnFilterProps) => import("react/jsx-runtime").JSX.Element;
19
+ export declare const FilterContainerColumnFilter: ({ column: columnProp, operator: operatorProp, defaultOperator, showOperatorPicker, operators, onOperatorChange, pattern: patternProp, variant, ...props }: FilterContainerColumnFilterProps) => import("react/jsx-runtime").JSX.Element;
20
20
  /**
21
21
  * FilterContainer is a generic UI container for a collection of Filter
22
22
  * controls. Each control manages a single filter clause and the Filter
@@ -2,6 +2,8 @@ import { ColumnFilterChangeHandler, ColumnFilterCommitHandler, ColumnFilterOp, C
2
2
  import { ColumnDescriptor } from "@vuu-ui/vuu-table-types";
3
3
  export interface ColumnFilterContextProps {
4
4
  filterContainerInstalled: boolean;
5
+ filter?: FilterContainerFilter;
6
+ getFilter?: () => FilterContainerFilter | undefined;
5
7
  filterProviderKey?: string;
6
8
  onChange?: ColumnFilterChangeHandler;
7
9
  onCommit?: ColumnFilterCommitHandler;
@@ -10,7 +12,7 @@ export interface ColumnFilterContextProps {
10
12
  }
11
13
  export declare const ColumnFilterContext: import("react").Context<ColumnFilterContextProps>;
12
14
  export declare function useFilterContext(column: ColumnDescriptor, throwIfNoContainer?: false): ColumnFilterContextProps;
13
- export declare function useFilterContext(column: ColumnDescriptor, throwIfNoContainer: true): Required<ColumnFilterContextProps>;
15
+ export declare function useFilterContext(column: ColumnDescriptor, throwIfNoContainer: true): Required<Omit<ColumnFilterContextProps, "filter">> & Pick<ColumnFilterContextProps, "filter">;
14
16
  export type FilterAppliedHandler<F extends Filter = Filter> = (filter: F) => void;
15
17
  export type ColumnFilterContainerHookProps = {
16
18
  filter?: FilterContainerFilter;
package/types/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { ColumnFilter, type ColumnFilterProps, } from "./column-filter/ColumnFilter";
2
+ export type { SelectableColumnFilterOp } from "./column-filter/column-filter-utils";
2
3
  export * from "./filter-bar";
3
4
  export * from "./filter-clause";
4
5
  export { ExtendedSingleValueFilterClause } from "./filter-container/ExtendedSingleValueFilterClause";