@wallarm-org/design-system 0.35.0 → 0.36.0-rc-feature-AS-970.1
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/dist/components/FilterInput/FilterInput.js +1 -1
- package/dist/components/FilterInput/FilterInputContext/types.d.ts +4 -0
- package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.d.ts +2 -0
- package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.js +4 -0
- package/dist/components/FilterInput/FilterInputField/FilterInputChip/FilterInputChip.js +11 -4
- package/dist/components/FilterInput/FilterInputField/FilterInputChip/classes.d.ts +1 -0
- package/dist/components/FilterInput/FilterInputField/FilterInputChip/classes.js +7 -2
- package/dist/components/FilterInput/FilterInputField/FilterInputField.js +9 -2
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/deriveAutocompleteValues.js +3 -1
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useBlurCommit.d.ts +4 -1
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useBlurCommit.js +12 -3
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipActions.js +2 -0
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipEditing.d.ts +1 -0
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipEditing.js +11 -2
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts +9 -2
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js +45 -4
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFocusManagement.d.ts +9 -2
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFocusManagement.js +18 -3
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useInputHandlers.d.ts +4 -1
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useInputHandlers.js +7 -2
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow.d.ts +4 -0
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow.js +66 -7
- package/dist/components/FilterInput/hooks/useFilterInputExpression/buildChips.js +7 -1
- package/dist/components/FilterInput/lib/index.d.ts +1 -1
- package/dist/components/FilterInput/lib/index.js +2 -2
- package/dist/components/FilterInput/lib/operators.d.ts +18 -1
- package/dist/components/FilterInput/lib/operators.js +19 -2
- package/dist/metadata/components.json +2 -2
- package/package.json +1 -1
|
@@ -49,7 +49,7 @@ const FilterInput = ({ fields: rawFields = [], value, onChange, placeholder = 'T
|
|
|
49
49
|
setInputText: autocomplete.setInputText,
|
|
50
50
|
closeMenu: autocomplete.closeAutocompleteMenu,
|
|
51
51
|
replaceExpression,
|
|
52
|
-
resetAutocompleteState: autocomplete.
|
|
52
|
+
resetAutocompleteState: autocomplete.resetAutocompleteState
|
|
53
53
|
});
|
|
54
54
|
const contextValue = useFilterInputContextValue({
|
|
55
55
|
chips,
|
|
@@ -23,6 +23,9 @@ export interface FilterInputContextValue {
|
|
|
23
23
|
onInputClick: () => void;
|
|
24
24
|
onGapClick: (conditionIndex: number, afterConnector: boolean) => void;
|
|
25
25
|
onChipClick: (chipId: string, segment: ChipSegment, anchorRect: DOMRect) => void;
|
|
26
|
+
/** Click on a segment of the *building* (in-progress) chip — re-opens the
|
|
27
|
+
* corresponding menu and enters inline-edit without committing the chip. */
|
|
28
|
+
onBuildingChipClick: (segment: ChipSegment, anchorRect: DOMRect) => void;
|
|
26
29
|
onConnectorChange: (chipId: string, value: 'and' | 'or') => void;
|
|
27
30
|
onChipRemove: (chipId: string) => void;
|
|
28
31
|
onClear: () => void;
|
|
@@ -33,6 +36,7 @@ export interface FilterInputContextValue {
|
|
|
33
36
|
onCancelSegmentEdit: () => void;
|
|
34
37
|
onCustomValueCommit: (customText: string) => void;
|
|
35
38
|
onCustomAttributeCommit: (customText: string) => void;
|
|
39
|
+
onCustomOperatorCommit: (customText: string) => void;
|
|
36
40
|
/** Ref to the currently open menu content element */
|
|
37
41
|
menuRef: RefObject<HTMLDivElement | null>;
|
|
38
42
|
/** Close autocomplete menu (used by connector chip to enforce single-dropdown constraint) */
|
|
@@ -10,6 +10,7 @@ interface AutocompleteForContext {
|
|
|
10
10
|
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
11
11
|
handleInputClick: () => void;
|
|
12
12
|
handleChipClick: (chipId: string, segment: ChipSegment, anchorRect: DOMRect) => void;
|
|
13
|
+
handleBuildingChipClick: (segment: ChipSegment, anchorRect: DOMRect) => void;
|
|
13
14
|
handleConnectorChange: (chipId: string, value: 'and' | 'or') => void;
|
|
14
15
|
handleChipRemove: (chipId: string) => void;
|
|
15
16
|
handleClear: () => void;
|
|
@@ -23,6 +24,7 @@ interface AutocompleteForContext {
|
|
|
23
24
|
cancelSegmentEdit: () => void;
|
|
24
25
|
handleCustomValueCommit: (customText: string) => void;
|
|
25
26
|
handleCustomAttributeCommit: (customText: string) => void;
|
|
27
|
+
handleCustomOperatorCommit: (customText: string) => void;
|
|
26
28
|
menuRef: RefObject<HTMLDivElement | null>;
|
|
27
29
|
closeAutocompleteMenu: () => void;
|
|
28
30
|
segmentAttributeInputRef: RefObject<HTMLInputElement | null>;
|
|
@@ -16,6 +16,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
|
|
|
16
16
|
onInputKeyDown: autocomplete.handleKeyDown,
|
|
17
17
|
onInputClick: autocomplete.handleInputClick,
|
|
18
18
|
onChipClick: autocomplete.handleChipClick,
|
|
19
|
+
onBuildingChipClick: autocomplete.handleBuildingChipClick,
|
|
19
20
|
onConnectorChange: autocomplete.handleConnectorChange,
|
|
20
21
|
onChipRemove: autocomplete.handleChipRemove,
|
|
21
22
|
onClear: autocomplete.handleClear,
|
|
@@ -26,6 +27,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
|
|
|
26
27
|
onCancelSegmentEdit: autocomplete.cancelSegmentEdit,
|
|
27
28
|
onCustomValueCommit: autocomplete.handleCustomValueCommit,
|
|
28
29
|
onCustomAttributeCommit: autocomplete.handleCustomAttributeCommit,
|
|
30
|
+
onCustomOperatorCommit: autocomplete.handleCustomOperatorCommit,
|
|
29
31
|
menuRef: autocomplete.menuRef,
|
|
30
32
|
closeAutocompleteMenu: autocomplete.closeAutocompleteMenu,
|
|
31
33
|
registerChipRef,
|
|
@@ -44,6 +46,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
|
|
|
44
46
|
autocomplete.handleKeyDown,
|
|
45
47
|
autocomplete.handleInputClick,
|
|
46
48
|
autocomplete.handleChipClick,
|
|
49
|
+
autocomplete.handleBuildingChipClick,
|
|
47
50
|
autocomplete.handleConnectorChange,
|
|
48
51
|
autocomplete.handleChipRemove,
|
|
49
52
|
autocomplete.handleClear,
|
|
@@ -54,6 +57,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
|
|
|
54
57
|
autocomplete.cancelSegmentEdit,
|
|
55
58
|
autocomplete.handleCustomValueCommit,
|
|
56
59
|
autocomplete.handleCustomAttributeCommit,
|
|
60
|
+
autocomplete.handleCustomOperatorCommit,
|
|
57
61
|
autocomplete.menuRef,
|
|
58
62
|
autocomplete.closeAutocompleteMenu,
|
|
59
63
|
autocomplete.segmentAttributeInputRef,
|
|
@@ -8,11 +8,11 @@ import { FilterInputRemoveButton } from "./FilterInputRemoveButton.js";
|
|
|
8
8
|
import { Segment } from "./Segment.js";
|
|
9
9
|
import { SEGMENT_VARIANT } from "./segmentVariant.js";
|
|
10
10
|
const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = false, valueParts, valueSeparator, errorValueIndices, building = false, disabled = false, onRemove, onSegmentClick, className, ...props })=>{
|
|
11
|
-
const interactive = !
|
|
11
|
+
const interactive = !disabled;
|
|
12
12
|
const hasError = !!error;
|
|
13
13
|
const internalRef = useRef(null);
|
|
14
14
|
const editing = useEditingContext();
|
|
15
|
-
const isEditingThisChip = null != editing && null != chipId && editing.editingChipId === chipId;
|
|
15
|
+
const isEditingThisChip = null != editing && null != editing.editingSegment && (building ? null == editing.editingChipId : null != chipId && editing.editingChipId === chipId);
|
|
16
16
|
const activeSegment = isEditingThisChip ? editing.editingSegment : null;
|
|
17
17
|
const handleSegmentClick = useCallback((segment, e)=>{
|
|
18
18
|
if (!onSegmentClick) return;
|
|
@@ -25,6 +25,9 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
|
|
|
25
25
|
onSegmentClick,
|
|
26
26
|
activeSegment
|
|
27
27
|
]);
|
|
28
|
+
const handleSegmentMouseDown = useCallback((e)=>{
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
}, []);
|
|
28
31
|
const segmentEditProps = (segment)=>isEditingThisChip && editing.editingSegment === segment ? {
|
|
29
32
|
editing: true,
|
|
30
33
|
editText: editing.segmentFilterText,
|
|
@@ -44,7 +47,8 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
|
|
|
44
47
|
className: cn(chipVariants({
|
|
45
48
|
error: hasError,
|
|
46
49
|
interactive,
|
|
47
|
-
disabled
|
|
50
|
+
disabled,
|
|
51
|
+
building
|
|
48
52
|
}), 'max-w-[320px]', className),
|
|
49
53
|
"data-slot": "filter-input-condition-chip",
|
|
50
54
|
...props,
|
|
@@ -54,6 +58,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
|
|
|
54
58
|
className: "shrink-0",
|
|
55
59
|
error: true === error || error === SEGMENT_VARIANT.attribute,
|
|
56
60
|
onClick: interactive ? (e)=>handleSegmentClick(SEGMENT_VARIANT.attribute, e) : void 0,
|
|
61
|
+
onMouseDown: interactive && building ? handleSegmentMouseDown : void 0,
|
|
57
62
|
...segmentEditProps(SEGMENT_VARIANT.attribute),
|
|
58
63
|
children: attribute
|
|
59
64
|
}),
|
|
@@ -61,6 +66,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
|
|
|
61
66
|
variant: SEGMENT_VARIANT.operator,
|
|
62
67
|
className: "shrink-0",
|
|
63
68
|
onClick: interactive ? (e)=>handleSegmentClick(SEGMENT_VARIANT.operator, e) : void 0,
|
|
69
|
+
onMouseDown: interactive && building ? handleSegmentMouseDown : void 0,
|
|
64
70
|
...segmentEditProps(SEGMENT_VARIANT.operator),
|
|
65
71
|
children: operator ?? ''
|
|
66
72
|
}),
|
|
@@ -72,10 +78,11 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
|
|
|
72
78
|
valueSeparator: valueSeparator,
|
|
73
79
|
errorValueIndices: errorValueIndices,
|
|
74
80
|
onClick: interactive ? (e)=>handleSegmentClick(SEGMENT_VARIANT.value, e) : void 0,
|
|
81
|
+
onMouseDown: interactive && building ? handleSegmentMouseDown : void 0,
|
|
75
82
|
...segmentEditProps(SEGMENT_VARIANT.value),
|
|
76
83
|
children: value ?? ''
|
|
77
84
|
}),
|
|
78
|
-
building && /*#__PURE__*/ jsx(ChipSearchInput, {}),
|
|
85
|
+
building && !isEditingThisChip && /*#__PURE__*/ jsx(ChipSearchInput, {}),
|
|
79
86
|
onRemove && !disabled && /*#__PURE__*/ jsx(FilterInputRemoveButton, {
|
|
80
87
|
error: hasError,
|
|
81
88
|
onRemove: onRemove
|
|
@@ -3,6 +3,7 @@ export declare const chipVariants: (props?: ({
|
|
|
3
3
|
error?: boolean | null | undefined;
|
|
4
4
|
interactive?: boolean | null | undefined;
|
|
5
5
|
disabled?: boolean | null | undefined;
|
|
6
|
+
building?: boolean | null | undefined;
|
|
6
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
8
|
/** Segment container */
|
|
8
9
|
export declare const segmentContainer = "flex flex-col justify-center overflow-hidden leading-none";
|
|
@@ -14,11 +14,15 @@ const chipVariants = cva(`h-22 group/chip relative flex items-center justify-cen
|
|
|
14
14
|
disabled: {
|
|
15
15
|
true: 'opacity-50 cursor-default',
|
|
16
16
|
false: ''
|
|
17
|
+
},
|
|
18
|
+
building: {
|
|
19
|
+
true: '',
|
|
20
|
+
false: ''
|
|
17
21
|
}
|
|
18
22
|
},
|
|
19
23
|
compoundVariants: [
|
|
20
24
|
{
|
|
21
|
-
|
|
25
|
+
building: true,
|
|
22
26
|
error: false,
|
|
23
27
|
className: 'border-border-strong-primary'
|
|
24
28
|
}
|
|
@@ -26,7 +30,8 @@ const chipVariants = cva(`h-22 group/chip relative flex items-center justify-cen
|
|
|
26
30
|
defaultVariants: {
|
|
27
31
|
error: false,
|
|
28
32
|
interactive: false,
|
|
29
|
-
disabled: false
|
|
33
|
+
disabled: false,
|
|
34
|
+
building: false
|
|
30
35
|
}
|
|
31
36
|
});
|
|
32
37
|
const segmentContainer = 'flex flex-col justify-center overflow-hidden leading-none';
|
|
@@ -15,7 +15,7 @@ import { FilterInputSearch } from "./FilterInputSearch.js";
|
|
|
15
15
|
import { useChipsSplitting } from "./hooks/useChipsSplitting.js";
|
|
16
16
|
import { useExpandCollapse } from "./hooks/useExpandCollapse.js";
|
|
17
17
|
const FilterInputField = ({ className, ...props })=>{
|
|
18
|
-
const { chips, buildingChipData, buildingChipRef, insertIndex, insertAfterConnector, error, onInputClick, onGapClick, onChipClick, onConnectorChange, onChipRemove, editingChipId, editingSegment, segmentFilterText, onSegmentFilterChange, onCancelSegmentEdit, onCustomValueCommit, onCustomAttributeCommit, menuRef } = useFilterInputContext();
|
|
18
|
+
const { chips, buildingChipData, buildingChipRef, insertIndex, insertAfterConnector, error, onInputClick, onGapClick, onChipClick, onBuildingChipClick, onConnectorChange, onChipRemove, editingChipId, editingSegment, segmentFilterText, onSegmentFilterChange, onCancelSegmentEdit, onCustomValueCommit, onCustomAttributeCommit, onCustomOperatorCommit, menuRef } = useFilterInputContext();
|
|
19
19
|
const hasContent = chips.length > 0 || null != buildingChipData;
|
|
20
20
|
const { isExpanded, isOverflowing, innerRef, toggleExpand, multiRow } = useExpandCollapse();
|
|
21
21
|
const { chipsBefore, chipsAfter, hideTrailingGap, hideLeadingGap } = useChipsSplitting(chips, insertIndex, insertAfterConnector);
|
|
@@ -42,6 +42,11 @@ const FilterInputField = ({ className, ...props })=>{
|
|
|
42
42
|
onCustomAttributeCommit(segmentFilterText);
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
|
+
if (editingSegment === SEGMENT_VARIANT.operator) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
onCustomOperatorCommit(segmentFilterText);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
45
50
|
}
|
|
46
51
|
if ('ArrowDown' === e.key) {
|
|
47
52
|
e.preventDefault();
|
|
@@ -53,6 +58,7 @@ const FilterInputField = ({ className, ...props })=>{
|
|
|
53
58
|
segmentFilterText,
|
|
54
59
|
onCustomValueCommit,
|
|
55
60
|
onCustomAttributeCommit,
|
|
61
|
+
onCustomOperatorCommit,
|
|
56
62
|
menuRef
|
|
57
63
|
]);
|
|
58
64
|
const handleSegmentEditBlur = useCallback((e)=>{
|
|
@@ -108,9 +114,10 @@ const FilterInputField = ({ className, ...props })=>{
|
|
|
108
114
|
buildingChipData ? /*#__PURE__*/ jsx(FilterInputChip, {
|
|
109
115
|
ref: buildingChipRef,
|
|
110
116
|
building: true,
|
|
111
|
-
attribute: buildingChipData.attribute
|
|
117
|
+
attribute: buildingChipData.attribute,
|
|
112
118
|
operator: buildingChipData.operator,
|
|
113
119
|
value: buildingChipData.value,
|
|
120
|
+
onSegmentClick: onBuildingChipClick,
|
|
114
121
|
className: "mx-4"
|
|
115
122
|
}) : /*#__PURE__*/ jsx(FilterInputSearch, {
|
|
116
123
|
hasContent: hasContent
|
package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/deriveAutocompleteValues.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { chipIdToConditionIndex, getDateDisplayLabel, getFieldValues, getOperatorLabel, hasStaticAllowlist, isMultiSelectOperator } from "../../lib/index.js";
|
|
1
|
+
import { chipIdToConditionIndex, getDateDisplayLabel, getFieldValues, getOperatorLabel, hasStaticAllowlist, isMultiSelectOperator, isNoValueOperator } from "../../lib/index.js";
|
|
2
|
+
const NO_VALUE_PLACEHOLDER = '—';
|
|
2
3
|
const getEditingCondition = (editingChipId, conditions)=>{
|
|
3
4
|
if (!editingChipId) return null;
|
|
4
5
|
const idx = chipIdToConditionIndex(editingChipId);
|
|
@@ -32,6 +33,7 @@ const deriveAutocompleteValues = ({ editingChipId, selectedField, selectedOperat
|
|
|
32
33
|
const buildingValue = (()=>{
|
|
33
34
|
if (buildingMultiValue) return buildingMultiValue;
|
|
34
35
|
if (dateRangeFromValue && 'between' === selectedOperator) return `${getDateDisplayLabel(dateRangeFromValue)} – ...`;
|
|
36
|
+
if (selectedOperator && isNoValueOperator(selectedOperator)) return NO_VALUE_PLACEHOLDER;
|
|
35
37
|
})();
|
|
36
38
|
const buildingChipData = isBuilding ? {
|
|
37
39
|
attribute: selectedField.label,
|
|
@@ -29,5 +29,8 @@ interface UseBlurCommitDeps {
|
|
|
29
29
|
* plus a blur in the same tick) short-circuit on re-entry instead of creating
|
|
30
30
|
* duplicate error chips.
|
|
31
31
|
*/
|
|
32
|
-
export declare const useBlurCommit: ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef, }: UseBlurCommitDeps) =>
|
|
32
|
+
export declare const useBlurCommit: ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef, }: UseBlurCommitDeps) => {
|
|
33
|
+
commitBuildingOnBlur: () => boolean;
|
|
34
|
+
hasIncompleteBuilding: () => boolean;
|
|
35
|
+
};
|
|
33
36
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
|
+
import { isBuildingComplete, isNoValueOperator } from "../../lib/index.js";
|
|
2
3
|
const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef })=>{
|
|
3
4
|
const selectedFieldRef = useRef(selectedField);
|
|
4
5
|
selectedFieldRef.current = selectedField;
|
|
@@ -14,16 +15,18 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
|
|
|
14
15
|
const text = inputTextRef.current.trim();
|
|
15
16
|
if (!field) return false;
|
|
16
17
|
if (editingChipId) return false;
|
|
18
|
+
const hasTypedValue = !!operator && !isNoValueOperator(operator) && text.length > 0;
|
|
19
|
+
if (!isBuildingComplete(field, operator, null) && !hasTypedValue) return false;
|
|
17
20
|
committingRef.current = true;
|
|
18
21
|
try {
|
|
19
22
|
selectedFieldRef.current = null;
|
|
20
23
|
selectedOperatorRef.current = null;
|
|
21
24
|
inputTextRef.current = '';
|
|
22
|
-
if (
|
|
25
|
+
if (hasTypedValue) {
|
|
23
26
|
handleCustomValueCommit(text);
|
|
24
27
|
return true;
|
|
25
28
|
}
|
|
26
|
-
upsertCondition(field, operator
|
|
29
|
+
upsertCondition(field, operator, null, void 0, effectiveInsertIndexRef.current);
|
|
27
30
|
resetState();
|
|
28
31
|
return true;
|
|
29
32
|
} finally{
|
|
@@ -37,6 +40,12 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
|
|
|
37
40
|
effectiveInsertIndexRef
|
|
38
41
|
]);
|
|
39
42
|
commitBuildingOnBlurRef.current = commitBuildingOnBlur;
|
|
40
|
-
|
|
43
|
+
const hasIncompleteBuilding = useCallback(()=>null !== selectedFieldRef.current && !editingChipId, [
|
|
44
|
+
editingChipId
|
|
45
|
+
]);
|
|
46
|
+
return {
|
|
47
|
+
commitBuildingOnBlur,
|
|
48
|
+
hasIncompleteBuilding
|
|
49
|
+
};
|
|
41
50
|
};
|
|
42
51
|
export { useBlurCommit };
|
|
@@ -25,6 +25,7 @@ const useChipActions = ({ effectiveInsertIndexRef, inputRef, removeCondition, cl
|
|
|
25
25
|
resetState
|
|
26
26
|
]);
|
|
27
27
|
const handleGapClick = useCallback((conditionIndex, afterConnector)=>{
|
|
28
|
+
resetState();
|
|
28
29
|
flushSync(()=>{
|
|
29
30
|
setInsertIndex(conditionIndex);
|
|
30
31
|
setInsertAfterConnector(afterConnector);
|
|
@@ -34,6 +35,7 @@ const useChipActions = ({ effectiveInsertIndexRef, inputRef, removeCondition, cl
|
|
|
34
35
|
setMenuState('field');
|
|
35
36
|
inputRef.current?.focus();
|
|
36
37
|
}, [
|
|
38
|
+
resetState,
|
|
37
39
|
resetMenuOffset,
|
|
38
40
|
inputRef,
|
|
39
41
|
setInsertIndex,
|
|
@@ -26,6 +26,7 @@ export declare const useChipEditing: ({ conditions, chips, fields, containerRef,
|
|
|
26
26
|
setSegmentFilterText: (text: string) => void;
|
|
27
27
|
resetSegmentTyping: () => void;
|
|
28
28
|
handleChipClick: (chipId: string, segment: ChipSegment, anchorRect: DOMRect) => void;
|
|
29
|
+
startBuildingEdit: (segment: ChipSegment, currentText: string) => void;
|
|
29
30
|
clearEditing: () => void;
|
|
30
31
|
};
|
|
31
32
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
|
|
3
|
-
import { chipIdToConditionIndex, getOperatorFromLabel } from "../../lib/index.js";
|
|
3
|
+
import { chipIdToConditionIndex, getOperatorFromLabel, isNoValueOperator } from "../../lib/index.js";
|
|
4
4
|
const getConditionByChipId = (chipId, conditions)=>{
|
|
5
5
|
const idx = chipIdToConditionIndex(chipId);
|
|
6
6
|
return null !== idx ? conditions[idx] ?? null : null;
|
|
@@ -35,7 +35,8 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
|
|
|
35
35
|
const chip = chipsRef.current.find((c)=>c.id === chipId);
|
|
36
36
|
if (!chip || 'chip' !== chip.variant) return;
|
|
37
37
|
const incompleteSegment = condition.error ? getFirstIncompleteSegment(condition, fieldsRef.current) : null;
|
|
38
|
-
const
|
|
38
|
+
const isPlaceholderValueClick = segment === SEGMENT_VARIANT.value && null != condition.operator && isNoValueOperator(condition.operator);
|
|
39
|
+
const targetSegment = incompleteSegment ?? (isPlaceholderValueClick ? SEGMENT_VARIANT.operator : segment);
|
|
39
40
|
if (!field && targetSegment !== SEGMENT_VARIANT.attribute) return;
|
|
40
41
|
if (incompleteSegment && field) upsertCondition(field, condition.operator, condition.value, chipId);
|
|
41
42
|
const containerRect = containerRef.current?.getBoundingClientRect();
|
|
@@ -67,6 +68,12 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
|
|
|
67
68
|
setSegmentFilterText('');
|
|
68
69
|
setUserHasTyped(false);
|
|
69
70
|
}, []);
|
|
71
|
+
const startBuildingEdit = useCallback((segment, currentText)=>{
|
|
72
|
+
setEditingChipId(null);
|
|
73
|
+
setEditingSegment(segment);
|
|
74
|
+
setSegmentFilterText(currentText);
|
|
75
|
+
setUserHasTyped(false);
|
|
76
|
+
}, []);
|
|
70
77
|
const handleSegmentFilterChange = useCallback((text)=>{
|
|
71
78
|
setSegmentFilterText(text);
|
|
72
79
|
setUserHasTyped(true);
|
|
@@ -85,6 +92,7 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
|
|
|
85
92
|
setSegmentFilterText: handleSegmentFilterChange,
|
|
86
93
|
resetSegmentTyping,
|
|
87
94
|
handleChipClick,
|
|
95
|
+
startBuildingEdit,
|
|
88
96
|
clearEditing
|
|
89
97
|
}), [
|
|
90
98
|
editingChipId,
|
|
@@ -94,6 +102,7 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
|
|
|
94
102
|
handleSegmentFilterChange,
|
|
95
103
|
resetSegmentTyping,
|
|
96
104
|
handleChipClick,
|
|
105
|
+
startBuildingEdit,
|
|
97
106
|
clearEditing
|
|
98
107
|
]);
|
|
99
108
|
};
|
package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RefObject } from 'react';
|
|
2
|
+
import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
|
|
2
3
|
import type { Condition, FieldMetadata, FilterInputChipData, FilterOperator, MenuState, UpsertCondition } from '../../types';
|
|
3
4
|
interface UseFilterInputAutocompleteOptions {
|
|
4
5
|
fields: FieldMetadata[];
|
|
@@ -46,8 +47,13 @@ export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, u
|
|
|
46
47
|
handleBuildingValueChange: (preview: string | undefined) => void;
|
|
47
48
|
handleMultiSelectToggle: () => void;
|
|
48
49
|
handleMenuClose: () => void;
|
|
49
|
-
handleMenuDiscard: (
|
|
50
|
-
|
|
50
|
+
handleMenuDiscard: () => void;
|
|
51
|
+
/** Hard reset of autocomplete state — used by paste/clipboard flows where
|
|
52
|
+
* the conditions array is replaced and any in-progress building must be
|
|
53
|
+
* scrapped, regardless of inline-edit mode. */
|
|
54
|
+
resetAutocompleteState: (continueBuilding?: boolean) => void;
|
|
55
|
+
handleChipClick: (chipId: string, segment: ChipSegment, anchorRect: DOMRect) => void;
|
|
56
|
+
handleBuildingChipClick: (segment: ChipSegment, anchorRect: DOMRect) => void;
|
|
51
57
|
handleConnectorChange: (connectorId: string, value: "and" | "or") => void;
|
|
52
58
|
handleChipRemove: (chipId: string) => void;
|
|
53
59
|
handleClear: () => void;
|
|
@@ -67,6 +73,7 @@ export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, u
|
|
|
67
73
|
cancelSegmentEdit: () => void;
|
|
68
74
|
handleCustomValueCommit: (customText: string) => void;
|
|
69
75
|
handleCustomAttributeCommit: (customText: string) => void;
|
|
76
|
+
handleCustomOperatorCommit: (customText: string) => void;
|
|
70
77
|
menuRef: RefObject<HTMLDivElement | null>;
|
|
71
78
|
closeAutocompleteMenu: () => void;
|
|
72
79
|
blurCommitRef: RefObject<(() => boolean) | null>;
|
package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useRef, useState } from "react";
|
|
2
2
|
import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
|
|
3
3
|
import { useDateRange } from "../../FilterInputMenu/FilterInputDateValueMenu/hooks.js";
|
|
4
|
-
import { applyAcceptChar } from "../../lib/index.js";
|
|
4
|
+
import { applyAcceptChar, getOperatorLabel } from "../../lib/index.js";
|
|
5
5
|
import { deriveAutocompleteValues } from "./deriveAutocompleteValues.js";
|
|
6
6
|
import { useBlurCommit } from "./useBlurCommit.js";
|
|
7
7
|
import { useChipActions } from "./useChipActions.js";
|
|
@@ -65,7 +65,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
|
|
|
65
65
|
setInsertAfterConnector,
|
|
66
66
|
setMenuState
|
|
67
67
|
});
|
|
68
|
-
const { handleMenuClose, handleFieldSelect, handleOperatorSelect, handleValueSelect, handleMultiCommit, handleBuildingValueChange, handleMultiSelectToggle, handleRangeSelect, handleCustomValueCommit, handleCustomAttributeCommit } = useMenuFlow({
|
|
68
|
+
const { handleMenuClose, handleFieldSelect, handleOperatorSelect, handleValueSelect, handleMultiCommit, handleBuildingValueChange, handleMultiSelectToggle, handleRangeSelect, handleCustomValueCommit, handleCustomAttributeCommit, handleCustomOperatorCommit } = useMenuFlow({
|
|
69
69
|
editing,
|
|
70
70
|
selectedField,
|
|
71
71
|
selectedOperator,
|
|
@@ -87,6 +87,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
|
|
|
87
87
|
inputText,
|
|
88
88
|
menuState,
|
|
89
89
|
selectedField,
|
|
90
|
+
selectedOperator,
|
|
90
91
|
isFocused,
|
|
91
92
|
fields,
|
|
92
93
|
inputRef,
|
|
@@ -102,7 +103,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
|
|
|
102
103
|
handleOperatorSelect,
|
|
103
104
|
handleCustomValueCommit
|
|
104
105
|
});
|
|
105
|
-
const commitBuildingOnBlur = useBlurCommit({
|
|
106
|
+
const { commitBuildingOnBlur, hasIncompleteBuilding } = useBlurCommit({
|
|
106
107
|
selectedField,
|
|
107
108
|
selectedOperator,
|
|
108
109
|
inputText,
|
|
@@ -118,6 +119,8 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
|
|
|
118
119
|
isFocused,
|
|
119
120
|
conditionsLength: conditions.length,
|
|
120
121
|
inputText,
|
|
122
|
+
selectedField,
|
|
123
|
+
selectedOperator,
|
|
121
124
|
containerRef,
|
|
122
125
|
inputRef,
|
|
123
126
|
editingSegment: editing.editingSegment,
|
|
@@ -126,6 +129,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
|
|
|
126
129
|
segmentValueInputRef,
|
|
127
130
|
blurCommitRef,
|
|
128
131
|
commitBuildingOnBlur,
|
|
132
|
+
hasIncompleteBuilding,
|
|
129
133
|
setIsFocused,
|
|
130
134
|
setMenuState,
|
|
131
135
|
resetMenuOffset,
|
|
@@ -160,6 +164,12 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
|
|
|
160
164
|
editing
|
|
161
165
|
]);
|
|
162
166
|
const cancelSegmentEdit = useCallback(()=>{
|
|
167
|
+
const isBuildingEdit = !editing.editingChipId && null !== editing.editingSegment;
|
|
168
|
+
if (isBuildingEdit) {
|
|
169
|
+
editing.clearEditing();
|
|
170
|
+
setMenuState('closed');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
163
173
|
setSelectedField(null);
|
|
164
174
|
setSelectedOperator(null);
|
|
165
175
|
editing.clearEditing();
|
|
@@ -167,6 +177,34 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
|
|
|
167
177
|
}, [
|
|
168
178
|
editing
|
|
169
179
|
]);
|
|
180
|
+
const handleMenuDiscard = useCallback(()=>{
|
|
181
|
+
const isBuildingEdit = !editing.editingChipId && null !== editing.editingSegment;
|
|
182
|
+
if (isBuildingEdit) {
|
|
183
|
+
editing.clearEditing();
|
|
184
|
+
setMenuState('closed');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
resetState();
|
|
188
|
+
}, [
|
|
189
|
+
editing,
|
|
190
|
+
resetState
|
|
191
|
+
]);
|
|
192
|
+
const handleBuildingChipClick = useCallback((segment, anchorRect)=>{
|
|
193
|
+
if (!selectedField) return;
|
|
194
|
+
const containerRect = containerRef.current?.getBoundingClientRect();
|
|
195
|
+
setMenuOffset(containerRect ? anchorRect.left - containerRect.left : 0);
|
|
196
|
+
const initialText = segment === SEGMENT_VARIANT.attribute ? selectedField.label : segment === SEGMENT_VARIANT.operator ? selectedOperator ? getOperatorLabel(selectedOperator, selectedField.type) : '' : buildingMultiValue ?? '';
|
|
197
|
+
editing.startBuildingEdit(segment, initialText);
|
|
198
|
+
setInputText('');
|
|
199
|
+
setMenuState(segment === SEGMENT_VARIANT.attribute ? 'field' : segment === SEGMENT_VARIANT.operator ? 'operator' : 'value');
|
|
200
|
+
}, [
|
|
201
|
+
selectedField,
|
|
202
|
+
selectedOperator,
|
|
203
|
+
buildingMultiValue,
|
|
204
|
+
containerRef,
|
|
205
|
+
setMenuOffset,
|
|
206
|
+
editing
|
|
207
|
+
]);
|
|
170
208
|
return {
|
|
171
209
|
inputText,
|
|
172
210
|
menuState,
|
|
@@ -187,8 +225,10 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
|
|
|
187
225
|
handleBuildingValueChange,
|
|
188
226
|
handleMultiSelectToggle,
|
|
189
227
|
handleMenuClose,
|
|
190
|
-
handleMenuDiscard
|
|
228
|
+
handleMenuDiscard,
|
|
229
|
+
resetAutocompleteState: resetState,
|
|
191
230
|
handleChipClick: editing.handleChipClick,
|
|
231
|
+
handleBuildingChipClick,
|
|
192
232
|
handleConnectorChange: setConnectorValue,
|
|
193
233
|
handleChipRemove,
|
|
194
234
|
handleClear,
|
|
@@ -208,6 +248,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
|
|
|
208
248
|
cancelSegmentEdit,
|
|
209
249
|
handleCustomValueCommit,
|
|
210
250
|
handleCustomAttributeCommit,
|
|
251
|
+
handleCustomOperatorCommit,
|
|
211
252
|
menuRef,
|
|
212
253
|
closeAutocompleteMenu,
|
|
213
254
|
blurCommitRef,
|
package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFocusManagement.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { FocusEvent, RefObject } from 'react';
|
|
2
2
|
import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
|
|
3
|
-
import type { MenuState } from '../../types';
|
|
3
|
+
import type { FieldMetadata, FilterOperator, MenuState } from '../../types';
|
|
4
4
|
interface UseFocusManagementDeps {
|
|
5
5
|
menuState: MenuState;
|
|
6
6
|
isFocused: boolean;
|
|
7
7
|
conditionsLength: number;
|
|
8
8
|
inputText: string;
|
|
9
|
+
/** In-progress building-chip state — needed so refocus resumes at the next
|
|
10
|
+
* missing segment instead of (incorrectly) reopening the field menu. */
|
|
11
|
+
selectedField: FieldMetadata | null;
|
|
12
|
+
selectedOperator: FilterOperator | null;
|
|
9
13
|
containerRef: RefObject<HTMLElement | null>;
|
|
10
14
|
inputRef: RefObject<HTMLInputElement | null>;
|
|
11
15
|
editingSegment: ChipSegment | null;
|
|
@@ -19,12 +23,15 @@ interface UseFocusManagementDeps {
|
|
|
19
23
|
blurCommitRef: RefObject<(() => boolean) | null>;
|
|
20
24
|
/** Tries to commit a building chip's freeform value on blur. Returns true if committed. */
|
|
21
25
|
commitBuildingOnBlur: () => boolean;
|
|
26
|
+
/** True if there's an in-progress building chip that the blur/close path
|
|
27
|
+
* must preserve (skip resetState). */
|
|
28
|
+
hasIncompleteBuilding: () => boolean;
|
|
22
29
|
setIsFocused: (focused: boolean) => void;
|
|
23
30
|
setMenuState: (state: MenuState) => void;
|
|
24
31
|
resetMenuOffset: () => void;
|
|
25
32
|
resetState: (continueBuilding?: boolean) => void;
|
|
26
33
|
}
|
|
27
|
-
export declare const useFocusManagement: ({ menuState, isFocused, conditionsLength, inputText, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, setIsFocused, setMenuState, resetMenuOffset, resetState, }: UseFocusManagementDeps) => {
|
|
34
|
+
export declare const useFocusManagement: ({ menuState, isFocused, conditionsLength, inputText, selectedField, selectedOperator, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, hasIncompleteBuilding, setIsFocused, setMenuState, resetMenuOffset, resetState, }: UseFocusManagementDeps) => {
|
|
28
35
|
handleFocus: (e: FocusEvent) => void;
|
|
29
36
|
handleBlur: (e: FocusEvent) => void;
|
|
30
37
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
|
|
3
3
|
import { isMenuRelated } from "../../lib/index.js";
|
|
4
|
-
const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, setIsFocused, setMenuState, resetMenuOffset, resetState })=>{
|
|
4
|
+
const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText, selectedField, selectedOperator, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, hasIncompleteBuilding, setIsFocused, setMenuState, resetMenuOffset, resetState })=>{
|
|
5
5
|
const handleFocus = useCallback((e)=>{
|
|
6
6
|
if (e.target?.closest?.('[data-slot="filter-input-connector-chip"]')) return;
|
|
7
7
|
setIsFocused(true);
|
|
@@ -18,7 +18,7 @@ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText,
|
|
|
18
18
|
try {
|
|
19
19
|
setIsFocused(false);
|
|
20
20
|
const committed = blurCommitRef.current?.() || commitBuildingOnBlur();
|
|
21
|
-
if (!committed) resetState();
|
|
21
|
+
if (!committed && !hasIncompleteBuilding()) resetState();
|
|
22
22
|
related?.focus();
|
|
23
23
|
if (document.activeElement === inputRef.current) inputRef.current?.blur();
|
|
24
24
|
} finally{
|
|
@@ -28,13 +28,25 @@ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText,
|
|
|
28
28
|
containerRef,
|
|
29
29
|
blurCommitRef,
|
|
30
30
|
commitBuildingOnBlur,
|
|
31
|
+
hasIncompleteBuilding,
|
|
31
32
|
resetState,
|
|
32
33
|
setIsFocused,
|
|
33
34
|
inputRef
|
|
34
35
|
]);
|
|
35
36
|
const prevFocusedRef = useRef(false);
|
|
36
37
|
useEffect(()=>{
|
|
37
|
-
if (isFocused
|
|
38
|
+
if (!isFocused || prevFocusedRef.current) {
|
|
39
|
+
prevFocusedRef.current = isFocused;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (editingSegment) {
|
|
43
|
+
prevFocusedRef.current = isFocused;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (selectedField) {
|
|
47
|
+
resetMenuOffset();
|
|
48
|
+
setMenuState(selectedOperator ? 'value' : 'operator');
|
|
49
|
+
} else if (0 === conditionsLength && '' === inputText) {
|
|
38
50
|
resetMenuOffset();
|
|
39
51
|
setMenuState('field');
|
|
40
52
|
}
|
|
@@ -43,6 +55,9 @@ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText,
|
|
|
43
55
|
isFocused,
|
|
44
56
|
conditionsLength,
|
|
45
57
|
inputText,
|
|
58
|
+
selectedField,
|
|
59
|
+
selectedOperator,
|
|
60
|
+
editingSegment,
|
|
46
61
|
resetMenuOffset,
|
|
47
62
|
setMenuState
|
|
48
63
|
]);
|
|
@@ -4,6 +4,9 @@ interface UseInputHandlersDeps {
|
|
|
4
4
|
inputText: string;
|
|
5
5
|
menuState: MenuState;
|
|
6
6
|
selectedField: FieldMetadata | null;
|
|
7
|
+
/** Needed so a click into the main input while a building chip is alive
|
|
8
|
+
* resumes at the next missing segment instead of doing nothing. */
|
|
9
|
+
selectedOperator: FilterOperator | null;
|
|
7
10
|
isFocused: boolean;
|
|
8
11
|
fields: FieldMetadata[];
|
|
9
12
|
inputRef: RefObject<HTMLInputElement | null>;
|
|
@@ -19,7 +22,7 @@ interface UseInputHandlersDeps {
|
|
|
19
22
|
handleOperatorSelect: (operator: FilterOperator) => void;
|
|
20
23
|
handleCustomValueCommit: (text: string) => void;
|
|
21
24
|
}
|
|
22
|
-
export declare const useInputHandlers: ({ inputText, menuState, selectedField, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit, }: UseInputHandlersDeps) => {
|
|
25
|
+
export declare const useInputHandlers: ({ inputText, menuState, selectedField, selectedOperator, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit, }: UseInputHandlersDeps) => {
|
|
23
26
|
handleInputChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
24
27
|
handleInputClick: () => void;
|
|
25
28
|
handleKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
2
|
import { OPERATOR_SYMBOLS, applyAcceptChar, getOperatorFromLabel, hasFieldValues } from "../../lib/index.js";
|
|
3
|
-
const useInputHandlers = ({ inputText, menuState, selectedField, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit })=>{
|
|
3
|
+
const useInputHandlers = ({ inputText, menuState, selectedField, selectedOperator, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit })=>{
|
|
4
4
|
const menuRef = useRef(null);
|
|
5
5
|
const handleInputChange = useCallback((e)=>{
|
|
6
6
|
let text = e.target.value;
|
|
@@ -21,13 +21,18 @@ const useInputHandlers = ({ inputText, menuState, selectedField, isFocused, fiel
|
|
|
21
21
|
]);
|
|
22
22
|
const handleInputClick = useCallback(()=>{
|
|
23
23
|
inputRef.current?.focus();
|
|
24
|
-
if ('closed'
|
|
24
|
+
if ('closed' !== menuState) return;
|
|
25
|
+
if (!selectedField) {
|
|
25
26
|
resetMenuOffset();
|
|
26
27
|
setMenuState('field');
|
|
28
|
+
return;
|
|
27
29
|
}
|
|
30
|
+
resetMenuOffset();
|
|
31
|
+
setMenuState(selectedOperator ? 'value' : 'operator');
|
|
28
32
|
}, [
|
|
29
33
|
menuState,
|
|
30
34
|
selectedField,
|
|
35
|
+
selectedOperator,
|
|
31
36
|
resetMenuOffset,
|
|
32
37
|
inputRef,
|
|
33
38
|
setMenuState
|
|
@@ -8,6 +8,9 @@ interface MenuFlowDeps {
|
|
|
8
8
|
setEditingSegment: (segment: ChipSegment | null) => void;
|
|
9
9
|
setSegmentFilterText: (text: string) => void;
|
|
10
10
|
resetSegmentTyping: () => void;
|
|
11
|
+
/** Exit inline-edit and the building-edit marker. Called when switching
|
|
12
|
+
* filter/operator in the building chip lands on the next menu. */
|
|
13
|
+
clearEditing: () => void;
|
|
11
14
|
};
|
|
12
15
|
selectedField: FieldMetadata | null;
|
|
13
16
|
selectedOperator: FilterOperator | null;
|
|
@@ -38,6 +41,7 @@ export declare const useMenuFlow: ({ editing, selectedField, selectedOperator, f
|
|
|
38
41
|
handleMultiSelectToggle: () => void;
|
|
39
42
|
handleRangeSelect: (from: string, to: string) => void;
|
|
40
43
|
handleCustomValueCommit: (customText: string) => void;
|
|
44
|
+
handleCustomOperatorCommit: (customText: string) => void;
|
|
41
45
|
handleCustomAttributeCommit: (customText: string) => void;
|
|
42
46
|
};
|
|
43
47
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
2
|
import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
|
|
3
|
-
import { chipIdToConditionIndex, isBetweenOperator, isMultiSelectOperator, isNoValueOperator } from "../../lib/index.js";
|
|
3
|
+
import { OPERATOR_SYMBOLS, chipIdToConditionIndex, getFieldOperators, getOperatorFromLabel, isBetweenOperator, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible } from "../../lib/index.js";
|
|
4
4
|
import { resolveDateRangeValue, resolveDateValue, resolveMultiValues, resolveSingleValue, validateValueForField } from "./valueCommitHelpers.js";
|
|
5
5
|
const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRef, insertIndex, upsertCondition, conditions, resetState, commitBuildingOnBlur, dateRange, setSelectedField, setSelectedOperator, setInputText, setMenuState, setBuildingMultiValue })=>{
|
|
6
6
|
const conditionsRef = useRef(conditions);
|
|
@@ -8,11 +8,17 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
|
|
|
8
8
|
const handleMenuClose = useCallback(()=>{
|
|
9
9
|
if (document.activeElement === inputRef.current) return;
|
|
10
10
|
if (document.activeElement?.closest?.('[data-slot^="segment-"]')) return;
|
|
11
|
-
if (
|
|
11
|
+
if (commitBuildingOnBlur()) return;
|
|
12
|
+
const hasIncompleteBuilding = null !== selectedField && !editing.editingChipId;
|
|
13
|
+
if (hasIncompleteBuilding) return void setMenuState('closed');
|
|
14
|
+
resetState();
|
|
12
15
|
}, [
|
|
13
16
|
commitBuildingOnBlur,
|
|
14
17
|
resetState,
|
|
15
|
-
inputRef
|
|
18
|
+
inputRef,
|
|
19
|
+
selectedField,
|
|
20
|
+
editing.editingChipId,
|
|
21
|
+
setMenuState
|
|
16
22
|
]);
|
|
17
23
|
const handleFieldSelect = useCallback((field)=>{
|
|
18
24
|
if (editing.editingChipId && editing.editingSegment === SEGMENT_VARIANT.attribute) {
|
|
@@ -25,19 +31,44 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
|
|
|
25
31
|
resetState();
|
|
26
32
|
return;
|
|
27
33
|
}
|
|
34
|
+
const isBuildingEdit = !editing.editingChipId && editing.editingSegment === SEGMENT_VARIANT.attribute;
|
|
35
|
+
if (isBuildingEdit) {
|
|
36
|
+
setSelectedField(field);
|
|
37
|
+
const keepOperator = selectedOperator ? isOperatorAllowedForField(field, selectedOperator) : false;
|
|
38
|
+
if (!keepOperator) setSelectedOperator(null);
|
|
39
|
+
editing.clearEditing();
|
|
40
|
+
setMenuState(keepOperator ? 'value' : 'operator');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
28
43
|
setSelectedField(field);
|
|
29
44
|
setInputText('');
|
|
30
45
|
setMenuState('operator');
|
|
31
46
|
}, [
|
|
32
47
|
editing,
|
|
48
|
+
selectedOperator,
|
|
33
49
|
upsertCondition,
|
|
34
50
|
resetState,
|
|
35
51
|
setSelectedField,
|
|
52
|
+
setSelectedOperator,
|
|
36
53
|
setInputText,
|
|
37
54
|
setMenuState
|
|
38
55
|
]);
|
|
39
56
|
const handleOperatorSelect = useCallback((operator)=>{
|
|
40
57
|
if (!selectedField) return;
|
|
58
|
+
const isBuildingEdit = !editing.editingChipId && editing.editingSegment === SEGMENT_VARIANT.operator;
|
|
59
|
+
if (isBuildingEdit) {
|
|
60
|
+
const shapeCompatible = isValueShapeCompatible(selectedOperator, operator);
|
|
61
|
+
if (!shapeCompatible) setBuildingMultiValue(void 0);
|
|
62
|
+
setSelectedOperator(operator);
|
|
63
|
+
editing.clearEditing();
|
|
64
|
+
if (isNoValueOperator(operator)) {
|
|
65
|
+
upsertCondition(selectedField, operator, null, null, insertIndex);
|
|
66
|
+
resetState(true);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
setMenuState('value');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
41
72
|
if (isNoValueOperator(operator)) {
|
|
42
73
|
const isEditing = !!editing.editingChipId;
|
|
43
74
|
upsertCondition(selectedField, operator, null, editing.editingChipId, isEditing ? void 0 : insertIndex);
|
|
@@ -65,11 +96,14 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
|
|
|
65
96
|
}, [
|
|
66
97
|
editing,
|
|
67
98
|
selectedField,
|
|
99
|
+
selectedOperator,
|
|
68
100
|
insertIndex,
|
|
69
101
|
upsertCondition,
|
|
70
102
|
resetState,
|
|
71
103
|
setSelectedOperator,
|
|
72
|
-
setMenuState
|
|
104
|
+
setMenuState,
|
|
105
|
+
setBuildingMultiValue,
|
|
106
|
+
setInputText
|
|
73
107
|
]);
|
|
74
108
|
const handleValueSelect = useCallback((val)=>{
|
|
75
109
|
if (!selectedField || !selectedOperator) return;
|
|
@@ -161,12 +195,16 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
|
|
|
161
195
|
resetState
|
|
162
196
|
]);
|
|
163
197
|
const handleCustomAttributeCommit = useCallback((customText)=>{
|
|
164
|
-
if (!
|
|
198
|
+
if (!customText.trim()) return;
|
|
165
199
|
const trimmed = customText.trim();
|
|
200
|
+
const matchedField = fields.find((f)=>f.label.toLowerCase() === trimmed.toLowerCase() || f.name.toLowerCase() === trimmed.toLowerCase());
|
|
201
|
+
if (!editing.editingChipId) {
|
|
202
|
+
if (matchedField) handleFieldSelect(matchedField);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
166
205
|
const idx = chipIdToConditionIndex(editing.editingChipId);
|
|
167
206
|
const condition = null !== idx ? conditionsRef.current[idx] : null;
|
|
168
207
|
if (!condition) return;
|
|
169
|
-
const matchedField = fields.find((f)=>f.label.toLowerCase() === trimmed.toLowerCase() || f.name.toLowerCase() === trimmed.toLowerCase());
|
|
170
208
|
if (matchedField) {
|
|
171
209
|
const hasValueError = validateValueForField(matchedField, condition.value);
|
|
172
210
|
upsertCondition(matchedField, condition.operator, condition.value, editing.editingChipId, void 0, hasValueError ? SEGMENT_VARIANT.value : void 0, condition.dateOrigin);
|
|
@@ -183,7 +221,27 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
|
|
|
183
221
|
editing,
|
|
184
222
|
fields,
|
|
185
223
|
upsertCondition,
|
|
186
|
-
resetState
|
|
224
|
+
resetState,
|
|
225
|
+
handleFieldSelect
|
|
226
|
+
]);
|
|
227
|
+
const handleCustomOperatorCommit = useCallback((customText)=>{
|
|
228
|
+
if (!selectedField || !customText.trim()) return;
|
|
229
|
+
const trimmed = customText.trim();
|
|
230
|
+
const allowed = getFieldOperators(selectedField);
|
|
231
|
+
let matched = getOperatorFromLabel(trimmed, selectedField.type);
|
|
232
|
+
if (!matched) {
|
|
233
|
+
const symbolMatch = allowed.find((op)=>OPERATOR_SYMBOLS[op].toLowerCase() === trimmed.toLowerCase());
|
|
234
|
+
if (symbolMatch) matched = symbolMatch;
|
|
235
|
+
}
|
|
236
|
+
if (!matched) {
|
|
237
|
+
const rawMatch = allowed.find((op)=>op.toLowerCase() === trimmed.toLowerCase());
|
|
238
|
+
if (rawMatch) matched = rawMatch;
|
|
239
|
+
}
|
|
240
|
+
if (!matched || !isOperatorAllowedForField(selectedField, matched)) return;
|
|
241
|
+
handleOperatorSelect(matched);
|
|
242
|
+
}, [
|
|
243
|
+
selectedField,
|
|
244
|
+
handleOperatorSelect
|
|
187
245
|
]);
|
|
188
246
|
return {
|
|
189
247
|
handleMenuClose,
|
|
@@ -195,6 +253,7 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
|
|
|
195
253
|
handleMultiSelectToggle,
|
|
196
254
|
handleRangeSelect,
|
|
197
255
|
handleCustomValueCommit,
|
|
256
|
+
handleCustomOperatorCommit,
|
|
198
257
|
handleCustomAttributeCommit
|
|
199
258
|
};
|
|
200
259
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
|
|
2
|
-
import { findOptionByValue, getDateDisplayLabel, getOperatorLabel } from "../../lib/index.js";
|
|
2
|
+
import { findOptionByValue, getDateDisplayLabel, getOperatorLabel, isNoValueOperator } from "../../lib/index.js";
|
|
3
3
|
import { getInvalidValueIndices } from "../useFilterInputAutocomplete/valueCommitHelpers.js";
|
|
4
4
|
const INVALID_DATE = 'Invalid Date';
|
|
5
5
|
const DATE_RANGE_SEPARATOR = ' – ';
|
|
6
6
|
const MULTI_VALUE_SEPARATOR = ', ';
|
|
7
7
|
const DEFAULT_FIELD_TYPE = 'string';
|
|
8
8
|
const DEFAULT_CONNECTOR = 'and';
|
|
9
|
+
const NO_VALUE_PLACEHOLDER = '—';
|
|
9
10
|
const chipId = (i)=>`chip-${i}`;
|
|
10
11
|
const connectorChip = (i, variant)=>({
|
|
11
12
|
id: `connector-${i}`,
|
|
@@ -84,6 +85,11 @@ const makeConditionChip = (i, conditions, fields, error)=>{
|
|
|
84
85
|
const chipError = condition.error || (error ? true : void 0);
|
|
85
86
|
const field = fields.find((f)=>f.name === condition.field);
|
|
86
87
|
const baseChip = buildBaseChip(i, condition, field);
|
|
88
|
+
if (condition.operator && isNoValueOperator(condition.operator)) return {
|
|
89
|
+
...baseChip,
|
|
90
|
+
value: NO_VALUE_PLACEHOLDER,
|
|
91
|
+
error: chipError
|
|
92
|
+
};
|
|
87
93
|
if (field?.type === 'date') return Array.isArray(condition.value) ? buildDateRangeChip(baseChip, condition, chipError) : buildDateChip(baseChip, condition, chipError);
|
|
88
94
|
if (Array.isArray(condition.value)) return buildMultiValueChip(baseChip, condition, field, chipError);
|
|
89
95
|
return {
|
|
@@ -9,7 +9,7 @@ export { buildContainerAnchoredRect, isMenuRelated } from './dom';
|
|
|
9
9
|
export { findOptionByValue, getFieldValues, hasFieldValues, hasStaticAllowlist } from './fields';
|
|
10
10
|
export { filterAndSort } from './filterSort';
|
|
11
11
|
export { getCurrentValueTokenText, getValueFilterText } from './menuFilterText';
|
|
12
|
-
export { getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isMultiSelectOperator, isNoValueOperator, } from './operators';
|
|
12
|
+
export { getFieldOperators, getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isBuildingComplete, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible, } from './operators';
|
|
13
13
|
export { type FilterParseError, isFilterParseError, parseExpression } from './parseExpression';
|
|
14
14
|
export { serializeExpression } from './serializeExpression';
|
|
15
15
|
export { createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSerializer, createStatusCodeSuggestions, createStatusCodeValidator, } from './statusCode';
|
|
@@ -8,8 +8,8 @@ import { buildContainerAnchoredRect, isMenuRelated } from "./dom.js";
|
|
|
8
8
|
import { findOptionByValue, getFieldValues, hasFieldValues, hasStaticAllowlist } from "./fields.js";
|
|
9
9
|
import { filterAndSort } from "./filterSort.js";
|
|
10
10
|
import { getCurrentValueTokenText, getValueFilterText } from "./menuFilterText.js";
|
|
11
|
-
import { getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isMultiSelectOperator, isNoValueOperator } from "./operators.js";
|
|
11
|
+
import { getFieldOperators, getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isBuildingComplete, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible } from "./operators.js";
|
|
12
12
|
import { isFilterParseError, parseExpression } from "./parseExpression/index.js";
|
|
13
13
|
import { serializeExpression } from "./serializeExpression.js";
|
|
14
14
|
import { createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSerializer, createStatusCodeSuggestions, createStatusCodeValidator } from "./statusCode/index.js";
|
|
15
|
-
export { CONNECTOR_ID_PATTERN, DATE_PRESETS, NO_VALUE_OPERATORS, OPERATORS_BY_TYPE, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE, OPERATOR_SYMBOLS, QUERY_BAR_SELECTOR, VARIANT_LABELS, applyAcceptChar, applyFieldValueTransforms, applyKnownFieldHelpers, buildContainerAnchoredRect, chipIdToConditionIndex, createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSerializer, createStatusCodeSuggestions, createStatusCodeValidator, filterAndSort, findChipSplitIndex, findOptionByValue, formatDateForChip, getCurrentValueTokenText, getDateDisplayLabel, getFieldValues, getKnownFieldSerializer, getOperatorFromLabel, getOperatorLabel, getValueFilterText, hasFieldValues, hasStaticAllowlist, isBetweenOperator, isDatePreset, isFilterParseError, isMenuRelated, isMultiSelectOperator, isNoValueOperator, parseExpression, serializeExpression };
|
|
15
|
+
export { CONNECTOR_ID_PATTERN, DATE_PRESETS, NO_VALUE_OPERATORS, OPERATORS_BY_TYPE, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE, OPERATOR_SYMBOLS, QUERY_BAR_SELECTOR, VARIANT_LABELS, applyAcceptChar, applyFieldValueTransforms, applyKnownFieldHelpers, buildContainerAnchoredRect, chipIdToConditionIndex, createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSerializer, createStatusCodeSuggestions, createStatusCodeValidator, filterAndSort, findChipSplitIndex, findOptionByValue, formatDateForChip, getCurrentValueTokenText, getDateDisplayLabel, getFieldOperators, getFieldValues, getKnownFieldSerializer, getOperatorFromLabel, getOperatorLabel, getValueFilterText, hasFieldValues, hasStaticAllowlist, isBetweenOperator, isBuildingComplete, isDatePreset, isFilterParseError, isMenuRelated, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible, parseExpression, serializeExpression };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FieldType, FilterOperator } from '../types';
|
|
1
|
+
import type { FieldMetadata, FieldType, FilterOperator } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Helper to get operator label for specific field type
|
|
4
4
|
*/
|
|
@@ -13,3 +13,20 @@ export declare const isMultiSelectOperator: (op: FilterOperator | null) => op is
|
|
|
13
13
|
export declare const isNoValueOperator: (op: FilterOperator) => boolean;
|
|
14
14
|
/** Check if operator is a between/range operator */
|
|
15
15
|
export declare const isBetweenOperator: (op: FilterOperator | null) => boolean;
|
|
16
|
+
/** Get the list of operators a field allows (custom override or full type list) */
|
|
17
|
+
export declare const getFieldOperators: (field: FieldMetadata) => FilterOperator[];
|
|
18
|
+
/** Check whether an operator is supported by a field's type/override list */
|
|
19
|
+
export declare const isOperatorAllowedForField: (field: FieldMetadata, operator: FilterOperator) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Decide whether the in-progress (field, operator, value) triple is fully
|
|
22
|
+
* built — i.e. has all the segments the chip needs. No-value operators
|
|
23
|
+
* (is_null / is_not_null) are complete without a value: the chip renders a
|
|
24
|
+
* value-placeholder in that slot so it still visually has three segments.
|
|
25
|
+
*/
|
|
26
|
+
export declare const isBuildingComplete: (field: FieldMetadata | null, operator: FilterOperator | null, value: string | number | boolean | Array<string | number | boolean> | null | undefined) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Check whether two operators handle values in compatible shapes
|
|
29
|
+
* (multi-select / between / no-value categories match), meaning a value
|
|
30
|
+
* preview built for `a` can be reused as-is for `b`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const isValueShapeCompatible: (a: FilterOperator | null, b: FilterOperator | null) => boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MULTI_SELECT_OPERATORS, NO_VALUE_OPERATORS, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE } from "./constants.js";
|
|
1
|
+
import { MULTI_SELECT_OPERATORS, NO_VALUE_OPERATORS, OPERATORS_BY_TYPE, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE } from "./constants.js";
|
|
2
2
|
const getOperatorLabel = (operator, fieldType)=>OPERATOR_LABELS_BY_TYPE[fieldType]?.[operator] ?? OPERATOR_LABELS[operator];
|
|
3
3
|
const getOperatorFromLabel = (label, fieldType)=>{
|
|
4
4
|
const typeLabels = OPERATOR_LABELS_BY_TYPE[fieldType];
|
|
@@ -10,4 +10,21 @@ const getOperatorFromLabel = (label, fieldType)=>{
|
|
|
10
10
|
const isMultiSelectOperator = (op)=>null !== op && MULTI_SELECT_OPERATORS.includes(op);
|
|
11
11
|
const isNoValueOperator = (op)=>NO_VALUE_OPERATORS.includes(op);
|
|
12
12
|
const isBetweenOperator = (op)=>'between' === op;
|
|
13
|
-
|
|
13
|
+
const getFieldOperators = (field)=>field.operators ?? OPERATORS_BY_TYPE[field.type].flat();
|
|
14
|
+
const isOperatorAllowedForField = (field, operator)=>getFieldOperators(field).includes(operator);
|
|
15
|
+
const isBuildingComplete = (field, operator, value)=>{
|
|
16
|
+
if (!field || !operator) return false;
|
|
17
|
+
if (isNoValueOperator(operator)) return true;
|
|
18
|
+
if (null == value) return false;
|
|
19
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
20
|
+
return '' !== value;
|
|
21
|
+
};
|
|
22
|
+
const isValueShapeCompatible = (a, b)=>{
|
|
23
|
+
if (a === b) return true;
|
|
24
|
+
if (null == a || null == b) return false;
|
|
25
|
+
if (isMultiSelectOperator(a) !== isMultiSelectOperator(b)) return false;
|
|
26
|
+
if (isBetweenOperator(a) !== isBetweenOperator(b)) return false;
|
|
27
|
+
if (isNoValueOperator(a) !== isNoValueOperator(b)) return false;
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
30
|
+
export { getFieldOperators, getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isBuildingComplete, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible };
|
package/package.json
CHANGED