Skip to content

Commit 0de842f

Browse files
feat(ASTBuilder): add aggregate fuzzy match filter (#774)
This commit introduces support for fuzzy match filtering in aggregate queries. Key changes include: * Defined a new AST node with dedicated children to represent the fuzzy filter configuration. * Created a generic fuzzy matching configuration object that can be injected into consuming components. * Reused and generalized the existing fuzzy match function model to support broader use cases. --------- Co-authored-by: Christopher Debove <[email protected]>
1 parent 383c87f commit 0de842f

23 files changed

+779
-272
lines changed

packages/app-builder/src/components/AstBuilder/OperandInfos.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import {
1010
type AggregationAstNode,
1111
isAggregation,
12-
isBinaryAggregationFilter,
12+
isUnaryAggregationFilter,
1313
} from '@app-builder/models/astNode/aggregation';
1414
import {
1515
type CustomListAccessAstNode,
@@ -187,7 +187,7 @@ function AggregatorDescription({ node }: AggregatorDescriptionProps) {
187187
{fieldName.constant ?? '...'}
188188
</p>
189189
<ViewingOperator operator={operator.constant} isFilter />
190-
{isBinaryAggregationFilter(filter) ? (
190+
{!isUnaryAggregationFilter(filter) ? (
191191
<ViewingAstBuilderOperand node={filter.namedChildren.value} />
192192
) : null}
193193
</div>

packages/app-builder/src/components/AstBuilder/edition/EditModal/EditModal.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { isAggregation } from '@app-builder/models/astNode/aggregation';
1+
import {
2+
isAggregation,
3+
isFuzzyMatchFilterOptionsAstNode,
4+
} from '@app-builder/models/astNode/aggregation';
25
import { type EditableAstNode } from '@app-builder/models/astNode/builder-ast-node';
36
import { isIsMultipleOf } from '@app-builder/models/astNode/multiple-of';
47
import {
@@ -14,6 +17,7 @@ import { getEvaluationForNode } from '../helpers';
1417
import { useRoot } from '../hooks/useRoot';
1518
import { AstBuilderNodeSharpFactory } from '../node-store';
1619
import { EditAggregation } from './modals/Aggregation/Aggregation';
20+
import { EditFuzzyMatchAggregation } from './modals/FuzzyMatchComparator/FuzzyMatchAggregation';
1721
import { EditFuzzyMatchComparator } from './modals/FuzzyMatchComparator/FuzzyMatchComparator';
1822
import { EditIsMultipleOf } from './modals/IsMultipleOf/IsMultipleOf';
1923
import { EditStringTemplate } from './modals/StringTemplate/StringTemplate';
@@ -54,6 +58,7 @@ export function OperandEditModal({ node, ...props }: OperandEditModalProps) {
5458
.when(isFuzzyMatchComparator, () => <EditFuzzyMatchComparator {...props} />)
5559
.when(isAggregation, () => <EditAggregation {...props} />)
5660
.when(isStringTemplateAstNode, () => <EditStringTemplate {...props} />)
61+
.when(isFuzzyMatchFilterOptionsAstNode, () => <EditFuzzyMatchAggregation {...props} />)
5762
.exhaustive()}
5863
</AstBuilderNodeSharpFactory.Provider>
5964
);

packages/app-builder/src/components/AstBuilder/edition/EditModal/modals/Aggregation/Aggregation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export function EditAggregation(props: Omit<OperandEditModalProps, 'node'>) {
8585
)}
8686
/> */}
8787
</div>
88-
<div className="grid grid-cols-[150px_1fr] gap-2">
88+
<div className="grid grid-cols-[240px_1fr] gap-2">
8989
<div>{t('scenarios:edit_aggregation.function_title')}</div>
9090
<div>{t('scenarios:edit_aggregation.object_field_title')}</div>
9191
<div className="flex flex-col gap-2">

packages/app-builder/src/components/AstBuilder/edition/EditModal/modals/Aggregation/EditFilters.tsx

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,32 @@
11
import { Callout } from '@app-builder/components';
2+
import { AstBuilderDataSharpFactory } from '@app-builder/components/AstBuilder/Provider';
23
import { RemoveButton } from '@app-builder/components/AstBuilder/styles/RemoveButton';
34
import { scenarioI18n } from '@app-builder/components/Scenario';
4-
import { type DataModel, NewUndefinedAstNode } from '@app-builder/models';
5+
import { type DataModel, isUndefinedAstNode, NewUndefinedAstNode } from '@app-builder/models';
56
import {
67
type AggregationAstNode,
78
aggregationFilterOperators,
89
type BinaryAggregationFilterAstNode,
10+
type ComplexAggregationFilterAstNode,
911
isBinaryAggregationFilter,
12+
isBinaryAggregationFilterOperator,
13+
isComplexAggregationFilter,
1014
isUnaryAggregationFilterOperator,
1115
NewAggregatorFilterAstNode,
16+
NewFuzzyMatchFilterOptionsAstNode,
1217
type UnaryAggregationFilterAstNode,
1318
} from '@app-builder/models/astNode/aggregation';
1419
import {
1520
isKnownOperandAstNode,
1621
type KnownOperandAstNode,
1722
} from '@app-builder/models/astNode/builder-ast-node';
1823
import { NewConstantAstNode } from '@app-builder/models/astNode/constant';
24+
import { getAstNodeDisplayName } from '@app-builder/services/ast-node/getAstNodeDisplayName';
25+
import { useFormatLanguage } from '@app-builder/utils/format';
1926
import clsx from 'clsx';
2027
import { Fragment, type ReactNode, useMemo, useState } from 'react';
2128
import { Trans, useTranslation } from 'react-i18next';
29+
import { match } from 'ts-pattern';
2230
import { Button, MenuCommand } from 'ui-design-system';
2331
import { Icon } from 'ui-icons';
2432

@@ -27,6 +35,7 @@ import { EditionEvaluationErrors } from '../../../EvaluationErrors';
2735
import { getErrorsForNode } from '../../../helpers';
2836
import { AstBuilderNodeSharpFactory } from '../../../node-store';
2937
import { OperatorSelect } from '../../../OperatorSelect';
38+
import { OperandEditModal } from '../../EditModal';
3039
import { type DataModelFieldOption, EditDataModelFieldTableMenu } from './EditDataModelField';
3140

3241
type EditFiltersProps = {
@@ -35,11 +44,15 @@ type EditFiltersProps = {
3544
};
3645
export function EditFilters({ aggregatedField, dataModel }: EditFiltersProps) {
3746
const { t } = useTranslation(scenarioI18n);
47+
const { t: stringifyContextT } = useTranslation(['common', 'scenarios']);
48+
const language = useFormatLanguage();
49+
const customLists = AstBuilderDataSharpFactory.select((s) => s.data.customLists);
3850
const nodeSharp = AstBuilderNodeSharpFactory.useSharp();
3951
const filters = nodeSharp.select(
4052
(s) => (s.node as AggregationAstNode).namedChildren.filters.children,
4153
);
4254
const evaluation = nodeSharp.select((s) => s.validation);
55+
const [filterEditedIndex, setEditedFilterIndex] = useState<number | null>(null);
4356

4457
const tableName = aggregatedField?.tableName;
4558
const options = useMemo(() => {
@@ -67,6 +80,7 @@ export function EditFilters({ aggregatedField, dataModel }: EditFiltersProps) {
6780
<div className="flex flex-col gap-2">
6881
{filters.map((filter, filterIndex) => {
6982
const binaryFilter = isBinaryAggregationFilter(filter);
83+
const complexFilter = isComplexAggregationFilter(filter);
7084
const isLastFilter = filterIndex === filters.length - 1;
7185
const filteredFieldErrors = getErrorsForNode(evaluation, [
7286
filter.namedChildren.fieldName.id,
@@ -77,6 +91,15 @@ export function EditFilters({ aggregatedField, dataModel }: EditFiltersProps) {
7791
? getErrorsForNode(evaluation, filter.namedChildren.value.id, true)
7892
: [];
7993

94+
const displayName =
95+
complexFilter && !isUndefinedAstNode(filter.namedChildren.value.namedChildren.value)
96+
? getAstNodeDisplayName(filter.namedChildren.value, {
97+
t: stringifyContextT,
98+
language,
99+
customLists,
100+
})
101+
: '...';
102+
80103
return (
81104
<Fragment key={filterIndex}>
82105
<div className="border-grey-90 flex flex-col gap-4 rounded-md border-[0.5px] p-4">
@@ -124,16 +147,50 @@ export function EditFilters({ aggregatedField, dataModel }: EditFiltersProps) {
124147
return;
125148
}
126149

127-
const valueNode =
128-
'value' in filter.namedChildren
129-
? filter.namedChildren.value
130-
: NewUndefinedAstNode();
131-
(filter as BinaryAggregationFilterAstNode).namedChildren = {
150+
if (isBinaryAggregationFilterOperator(operator)) {
151+
const valueNode = match(filter)
152+
.when(
153+
isBinaryAggregationFilter,
154+
(binFilter: BinaryAggregationFilterAstNode) =>
155+
binFilter.namedChildren.value,
156+
)
157+
.when(
158+
isComplexAggregationFilter,
159+
(compFilter: ComplexAggregationFilterAstNode) =>
160+
compFilter.namedChildren.value.namedChildren.value,
161+
)
162+
.otherwise(() => NewUndefinedAstNode());
163+
164+
(filter as BinaryAggregationFilterAstNode).namedChildren = {
165+
tableName: filter.namedChildren.tableName,
166+
fieldName: filter.namedChildren.fieldName,
167+
operator: NewConstantAstNode({ constant: operator }),
168+
value: valueNode,
169+
};
170+
return;
171+
}
172+
173+
const valueNode = match(filter)
174+
.when(
175+
isBinaryAggregationFilter,
176+
(binFilter: BinaryAggregationFilterAstNode) =>
177+
binFilter.namedChildren.value,
178+
)
179+
.when(
180+
isComplexAggregationFilter,
181+
(compFilter: ComplexAggregationFilterAstNode) =>
182+
compFilter.namedChildren.value.namedChildren.value,
183+
)
184+
.otherwise(() => NewUndefinedAstNode());
185+
186+
(filter as ComplexAggregationFilterAstNode).namedChildren = {
132187
tableName: filter.namedChildren.tableName,
133188
fieldName: filter.namedChildren.fieldName,
134189
operator: NewConstantAstNode({ constant: operator }),
135-
value: valueNode,
190+
value: NewFuzzyMatchFilterOptionsAstNode({ value: valueNode }),
136191
};
192+
193+
// TODO: Manage complex operator change
137194
});
138195
nodeSharp.actions.validate();
139196
}}
@@ -155,6 +212,27 @@ export function EditFilters({ aggregatedField, dataModel }: EditFiltersProps) {
155212
/>
156213
</>
157214
) : null}
215+
{complexFilter && filter.namedChildren.operator.constant ? (
216+
<>
217+
<Button
218+
variant="secondary"
219+
onClick={() => setEditedFilterIndex(filterIndex)}
220+
>
221+
{displayName}
222+
</Button>
223+
{filter.namedChildren.value && filterEditedIndex === filterIndex ? (
224+
<OperandEditModal
225+
node={filter.namedChildren.value}
226+
onSave={() => {
227+
setEditedFilterIndex(null);
228+
}}
229+
onCancel={() => {
230+
setEditedFilterIndex(null);
231+
}}
232+
/>
233+
) : null}
234+
</>
235+
) : null}
158236
</div>
159237
<RemoveButton
160238
onClick={() => {

packages/app-builder/src/components/AstBuilder/edition/EditModal/modals/FuzzyMatchComparator/EditAlgorithm.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
1-
import { type ConstantAstNode } from '@app-builder/models/astNode/constant';
21
import {
3-
editableFuzzyMatchAlgorithms,
2+
type BaseFuzzyMatchConfig,
43
type FuzzyMatchAlgorithm,
5-
getFuzzyMatchAlgorithmName,
6-
isEditableFuzzyMatchAlgorithm,
7-
} from '@app-builder/models/fuzzy-match';
4+
} from '@app-builder/models/fuzzy-match/baseFuzzyMatchConfig';
85
import { useCallbackRef } from '@marble/shared';
96
import { useTranslation } from 'react-i18next';
107
import { Select, Tooltip } from 'ui-design-system';
118
import { Icon } from 'ui-icons';
129

13-
import { EditionEvaluationErrors } from '../../../EvaluationErrors';
1410
import { operatorContainerClassnames } from '../../../OperatorSelect';
1511

1612
interface EditAlgorithmProps {
17-
node: ConstantAstNode<FuzzyMatchAlgorithm>;
13+
fuzzyMatchConfig: BaseFuzzyMatchConfig;
14+
algorithm: FuzzyMatchAlgorithm;
1815
onChange: (value: FuzzyMatchAlgorithm) => void;
1916
}
2017

21-
export function EditAlgorithm({ node, onChange }: EditAlgorithmProps) {
18+
export function EditAlgorithm({ fuzzyMatchConfig, algorithm, onChange }: EditAlgorithmProps) {
2219
const { t } = useTranslation(['common', 'scenarios']);
23-
const algorithm = node.constant;
2420
const onValueChange = useCallbackRef(onChange);
2521

26-
if (isEditableFuzzyMatchAlgorithm(algorithm)) {
22+
if (fuzzyMatchConfig.isEditableAlgorithm(algorithm)) {
2723
return (
2824
<div className="flex flex-1 flex-col gap-2">
2925
<label htmlFor="algorithm" className="text-m text-grey-00 font-normal">
@@ -45,15 +41,18 @@ export function EditAlgorithm({ node, onChange }: EditAlgorithmProps) {
4541
</Select.Trigger>
4642
<Select.Content className="max-h-60">
4743
<Select.Viewport>
48-
{editableFuzzyMatchAlgorithms.map((fuzzyMatchAlgorithm) => {
44+
{Array.from(fuzzyMatchConfig.editablesAlgorithms).map((fuzzyMatchAlgorithm) => {
4945
return (
5046
<Select.Item
5147
className="flex min-w-[110px] flex-col gap-1"
5248
key={fuzzyMatchAlgorithm}
5349
value={fuzzyMatchAlgorithm}
5450
>
5551
<Select.ItemText>
56-
<FuzzyMatchAlgorithmLabel fuzzyMatchAlgorithm={fuzzyMatchAlgorithm} />
52+
<FuzzyMatchAlgorithmLabel
53+
fuzzyMatchConfig={fuzzyMatchConfig}
54+
fuzzyMatchAlgorithm={fuzzyMatchAlgorithm}
55+
/>
5756
</Select.ItemText>
5857
<p className="text-s text-grey-50">
5958
{t(`scenarios:edit_fuzzy_match.algorithm.description.${fuzzyMatchAlgorithm}`)}
@@ -64,7 +63,6 @@ export function EditAlgorithm({ node, onChange }: EditAlgorithmProps) {
6463
</Select.Viewport>
6564
</Select.Content>
6665
</Select.Root>
67-
<EditionEvaluationErrors id={node.id} />
6866
</div>
6967
);
7068
}
@@ -75,22 +73,26 @@ export function EditAlgorithm({ node, onChange }: EditAlgorithmProps) {
7573
{t('scenarios:edit_fuzzy_match.threshold.label')}
7674
</span>
7775
<div className="bg-grey-98 border-grey-90 flex h-10 items-center justify-center rounded border p-2 text-center">
78-
<FuzzyMatchAlgorithmLabel fuzzyMatchAlgorithm={algorithm} />
76+
<FuzzyMatchAlgorithmLabel
77+
fuzzyMatchConfig={fuzzyMatchConfig}
78+
fuzzyMatchAlgorithm={algorithm}
79+
/>
7980
</div>
80-
<EditionEvaluationErrors id={node.id} />
8181
</div>
8282
);
8383
}
8484

8585
function FuzzyMatchAlgorithmLabel({
86+
fuzzyMatchConfig,
8687
fuzzyMatchAlgorithm,
8788
}: {
89+
fuzzyMatchConfig: BaseFuzzyMatchConfig;
8890
fuzzyMatchAlgorithm: FuzzyMatchAlgorithm;
8991
}) {
9092
const { t } = useTranslation(['common', 'scenarios']);
9193
return (
9294
<span className="text-s text-grey-00 font-semibold">
93-
{getFuzzyMatchAlgorithmName(t, fuzzyMatchAlgorithm)}
95+
{fuzzyMatchConfig.getAlgorithmName(t, fuzzyMatchAlgorithm)}
9496
</span>
9597
);
9698
}

packages/app-builder/src/components/AstBuilder/edition/EditModal/modals/FuzzyMatchComparator/EditLevel.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import {
2-
type FuzzyMatchComparatorLevel,
3-
fuzzyMatchComparatorLevelData,
4-
} from '@app-builder/models/fuzzy-match';
2+
type BaseFuzzyMatchConfig,
3+
type Level,
4+
} from '@app-builder/models/fuzzy-match/baseFuzzyMatchConfig';
55
import { useTranslation } from 'react-i18next';
66
import { Select } from 'ui-design-system';
77

88
import { operatorContainerClassnames } from '../../../OperatorSelect';
99

1010
interface EditLevelProps {
11-
level: FuzzyMatchComparatorLevel;
12-
setLevel: (level: FuzzyMatchComparatorLevel) => void;
11+
config: BaseFuzzyMatchConfig;
12+
level: Level;
13+
setLevel: (level: Level) => void;
1314
}
1415

15-
export function EditLevel({ level, setLevel }: EditLevelProps) {
16+
export function EditLevel({ config, level, setLevel }: EditLevelProps) {
1617
const { t } = useTranslation(['common', 'scenarios']);
1718

1819
return (
@@ -28,7 +29,7 @@ export function EditLevel({ level, setLevel }: EditLevelProps) {
2829
</Select.Trigger>
2930
<Select.Content className="max-h-60">
3031
<Select.Viewport>
31-
{fuzzyMatchComparatorLevelData.map(({ level }) => {
32+
{config.getLevels().map((level) => {
3233
return (
3334
<Select.Item className="min-w-[110px]" key={level} value={level}>
3435
<Select.ItemText>

0 commit comments

Comments
 (0)