Skip to content

Commit

Permalink
feat(builder): add fuzzy match functions (#421)
Browse files Browse the repository at this point in the history
* feat(models): add fuzzy match functions (#414)

* feat(builder): add fuzzy match functions edit (#415)

* feat(builder): add fuzzy match functions edit

* feat(builder): handle fuzzy match any of

* feat(data-type): handle list of data types

* feat(fuzzy-match): edit threshold (#419)

* feat(fuzzy-match): add doc for fuzzy match functions

* feat(ui-design-system): add switch focus styles

* feat(fuzzy-match): edit threshold

* feat(fuzzy-match): localised editable algorithm
  • Loading branch information
balzdur authored Apr 9, 2024
1 parent 484875a commit 1da5924
Show file tree
Hide file tree
Showing 27 changed files with 1,327 additions and 73 deletions.
23 changes: 22 additions & 1 deletion packages/app-builder/public/locales/en/scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,9 @@
"edit_operand.constant.use_data_type.number[]": "Use number list",
"edit_operand.constant.use_data_type.boolean[]": "Use boolean list",
"edit_operand.data_type.string": "String",
"edit_operand.data_type.string[]": "String list",
"edit_operand.data_type.number": "Number",
"edit_operand.data_type.number[]": "Number list",
"edit_operand.data_type.boolean": "Boolean",
"edit_operand.data_type.timestamp": "Timestamp",
"edit_operand.operator_type.list_one": "List",
Expand All @@ -214,5 +216,24 @@
"edit_date.now.description": "The time when the scenario is run",
"edit_date.date": "Date",
"enum_options": "Options:",
"nest": "Nest"
"nest": "Nest",
"edit_fuzzy_match.string_similarity": "String similarity",
"edit_fuzzy_match.fuzzy_match": "match",
"edit_fuzzy_match.fuzzy_match_any_of": "match any of",
"edit_fuzzy_match.title": "Create a text match",
"edit_fuzzy_match.description": "Match a text field against a list of values. You can find more information in our documentation <DocLink>here</DocLink>.",
"edit_fuzzy_match.algorithm.label": "Similarity algorithm",
"edit_fuzzy_match.algorithm.ratio": "full string similarity",
"edit_fuzzy_match.algorithm.token_set_ratio": "bag of words similarity",
"edit_fuzzy_match.algorithm.description.ratio": "Compare strings that are expected to be nearly equal",
"edit_fuzzy_match.algorithm.description.token_set_ratio": "Compare strings that are composed of the same words",
"edit_fuzzy_match.threshold.label": "Matching threshold",
"edit_fuzzy_match.level.label": "Matching level",
"edit_fuzzy_match.level.medium": "medium",
"edit_fuzzy_match.level.high": "high",
"edit_fuzzy_match.examples.caption": "examples",
"edit_fuzzy_match.examples.left": "left string",
"edit_fuzzy_match.examples.right": "right string",
"edit_fuzzy_match.examples.result": "result",
"edit_fuzzy_match.operands.label": "Text to match"
}
10 changes: 5 additions & 5 deletions packages/app-builder/src/components/Callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ const callout = cva(
},
);

interface CalloutProps extends VariantProps<typeof callout> {
children: React.ReactNode;
className?: string;
}
interface CalloutProps
extends VariantProps<typeof callout>,
Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {}

export function Callout({
children,
className,
color = 'purple',
variant = 'soft',
...otherProps
}: CalloutProps) {
if (!children) return null;

return (
<div className={callout({ color, variant, className })}>
<div className={callout({ color, variant, className })} {...otherProps}>
<Icon icon="lightbulb" className="size-6 shrink-0" />
{children}
</div>
Expand Down
22 changes: 22 additions & 0 deletions packages/app-builder/src/components/ExternalLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import clsx from 'clsx';
import { forwardRef } from 'react';

export const ExternalLink = forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<'a'>
>(function ExternalLink({ className, children, ...otherProps }, ref) {
return (
<a
ref={ref}
className={clsx(
'hover:text-purple-120 focus:text-purple-120 font-semibold lowercase text-purple-100 hover:underline focus:underline',
className,
)}
target="_blank"
rel="noreferrer"
{...otherProps}
>
{children}
</a>
);
});
20 changes: 1 addition & 19 deletions packages/app-builder/src/components/FormatData.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { formatDateTime, formatNumber } from '@app-builder/utils/format';
import type React from 'react';

export function ExternalLink({
href,
children,
}: {
href: string;
children: React.ReactNode;
}) {
return (
<a
className="hover:text-purple-120 focus:text-purple-120 font-semibold lowercase text-purple-100 hover:underline focus:underline"
target="_blank"
rel="noreferrer"
href={href}
>
{children}
</a>
);
}
import { ExternalLink } from './ExternalLink';

type Data =
| {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CopyPasteASTContextProvider } from '@app-builder/services/editor/copy-p
import { type CustomList } from 'marble-api';

import { AggregationEditModal } from './AstBuilderNode/AggregationEdit';
import { FuzzyMatchComparatorEditModal } from './AstBuilderNode/FuzzyMatchComparatorEdit/Modal';
import { TimeAddEditModal } from './AstBuilderNode/TimeAddEdit/Modal';
import { RootAstBuilderNode } from './RootAstBuilderNode';

Expand Down Expand Up @@ -45,14 +46,16 @@ export function AstBuilder({
<CopyPasteASTContextProvider>
<TimeAddEditModal>
<AggregationEditModal>
<RootAstBuilderNode
setOperand={setOperand}
setOperator={setOperator}
appendChild={appendChild}
remove={remove}
editorNodeViewModel={editorNodeViewModel}
viewOnly={viewOnly}
/>
<FuzzyMatchComparatorEditModal>
<RootAstBuilderNode
setOperand={setOperand}
setOperator={setOperator}
appendChild={appendChild}
remove={remove}
editorNodeViewModel={editorNodeViewModel}
viewOnly={viewOnly}
/>
</FuzzyMatchComparatorEditModal>
</AggregationEditModal>
</TimeAddEditModal>
</CopyPasteASTContextProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Callout } from '@app-builder/components';
import { scenarioI18n } from '@app-builder/components/Scenario';
import { EvaluationErrors } from '@app-builder/components/Scenario/ScenarioValidationError';
import { NewUndefinedAstNode } from '@app-builder/models';
import { type AstNode, NewUndefinedAstNode } from '@app-builder/models';
import {
filterOperators,
isFilterOperator,
} from '@app-builder/models/editable-operators';
import { useOperandOptions } from '@app-builder/services/ast-node/options';
import { adaptEditorNodeViewModel } from '@app-builder/services/editor/ast-editor';
import {
adaptEvaluationErrorViewModels,
Expand All @@ -19,7 +20,7 @@ import { Icon } from 'ui-icons';

import { RemoveButton } from '../../RemoveButton';
import { LogicalOperatorLabel } from '../../RootAstBuilderNode/LogicalOperator';
import { Operand } from '../Operand';
import { Operand, type OperandViewModel } from '../Operand';
import { Operator } from '../Operator';
import { type DataModelField, EditDataModelField } from './EditDataModelField';
import { type FilterViewModel } from './Modal';
Expand Down Expand Up @@ -136,8 +137,8 @@ export function EditFilters({
errors={filter.errors.operator}
operators={filterOperators}
/>
<Operand
operandViewModel={filter.value}
<FilterValue
filterValue={filter.value}
onSave={(astNode) =>
onFilterChange(
{ value: adaptEditorNodeViewModel({ ast: astNode }) },
Expand Down Expand Up @@ -170,3 +171,20 @@ export function EditFilters({
</div>
);
}

function FilterValue({
filterValue,
onSave,
}: {
filterValue: OperandViewModel;
onSave: (astNode: AstNode) => void;
}) {
const filterOptions = useOperandOptions({ operandViewModel: filterValue });
return (
<Operand
operandViewModel={filterValue}
onSave={onSave}
options={filterOptions}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export function AggregationEditModal({
<ModalV2.Root open={open} setOpen={onOpenChange}>
<AggregationEditModalContext.Provider value={editAgregation}>
{children}
<ModalV2.Content size="large">
<ModalV2.Content size="large" unmountOnHide>
{/* New context necessary, hack to prevent pasting unwanted astnode inside the modal (ex: I close the modal, copy the current node, open the modal and paste the current inside the current...) */}
<CopyPasteASTContextProvider>
{aggregation ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type AstNode } from '@app-builder/models';
import { useOperandOptions } from '@app-builder/services/ast-node/options';
import { type EditorNodeViewModel } from '@app-builder/services/editor/ast-editor';

import { Operand } from './Operand';
Expand Down Expand Up @@ -26,6 +27,7 @@ export function AstBuilderNode({
}: AstBuilderNodeProps) {
const twoOperandsViewModel =
adaptTwoOperandsLineViewModel(editorNodeViewModel);
const options = useOperandOptions({ operandViewModel: editorNodeViewModel });

if (twoOperandsViewModel) {
return (
Expand All @@ -46,6 +48,7 @@ export function AstBuilderNode({
operandViewModel={editorNodeViewModel}
viewOnly={viewOnly}
onSave={onSave}
options={options}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { EvaluationErrors } from '@app-builder/components/Scenario/ScenarioValidationError';
import {
editableFuzzyMatchAlgorithms,
type FuzzyMatchAlgorithm,
getFuzzyMatchAlgorithmName,
isEditableFuzzyMatchAlgorithm,
} from '@app-builder/models/fuzzy-match';
import { type EvaluationError } from '@app-builder/models/node-evaluation';
import {
adaptEvaluationErrorViewModels,
useGetNodeEvaluationErrorMessage,
} from '@app-builder/services/validation';
import { useTranslation } from 'react-i18next';
import { Select, Tooltip } from 'ui-design-system';
import { Icon } from 'ui-icons';

import { operatorContainerClassnames } from '../Operator';

interface EditAlgorithmProps {
algorithm: FuzzyMatchAlgorithm;
setAlgorithm: (algorithm: FuzzyMatchAlgorithm) => void;
errors: EvaluationError[];
}

export function EditAlgorithm({
algorithm,
setAlgorithm,
errors,
}: EditAlgorithmProps) {
const { t } = useTranslation(['common', 'scenarios']);
const getNodeEvaluationErrorMessage = useGetNodeEvaluationErrorMessage();

if (isEditableFuzzyMatchAlgorithm(algorithm)) {
return (
<div className="flex flex-1 flex-col gap-2">
<label htmlFor="algorithm" className="text-m text-grey-100 font-normal">
{t('scenarios:edit_fuzzy_match.algorithm.label')}
</label>
<Select.Root value={algorithm} onValueChange={setAlgorithm}>
<Select.Trigger
id="algorithm"
className={operatorContainerClassnames({
validationStatus: errors.length > 0 ? 'error' : 'valid',
})}
>
<span className="text-s text-grey-100 w-full text-center font-medium">
<Select.Value placeholder="..." />
</span>
<Tooltip.Default
content={t(
`scenarios:edit_fuzzy_match.algorithm.description.${algorithm}`,
)}
>
<Icon
icon="tip"
className="size-5 shrink-0 text-purple-50 transition-colors hover:text-purple-100"
/>
</Tooltip.Default>
</Select.Trigger>
<Select.Content className="max-h-60">
<Select.Viewport>
{editableFuzzyMatchAlgorithms.map((fuzzyMatchAlgorithm) => {
return (
<Select.Item
className="flex min-w-[110px] flex-col gap-1"
key={fuzzyMatchAlgorithm}
value={fuzzyMatchAlgorithm}
>
<Select.ItemText>
<FuzzyMatchAlgorithmLabel
fuzzyMatchAlgorithm={fuzzyMatchAlgorithm}
/>
</Select.ItemText>
<p className="text-s text-grey-50">
{t(
`scenarios:edit_fuzzy_match.algorithm.description.${fuzzyMatchAlgorithm}`,
)}
</p>
</Select.Item>
);
})}
</Select.Viewport>
</Select.Content>
</Select.Root>
<EvaluationErrors
errors={adaptEvaluationErrorViewModels(errors).map(
getNodeEvaluationErrorMessage,
)}
/>
</div>
);
}

return (
<div className="flex flex-1 flex-col gap-2">
<span className="text-m text-grey-100 font-normal">
{t('scenarios:edit_fuzzy_match.threshold.label')}
</span>
<div className="bg-grey-02 border-grey-10 flex h-10 items-center justify-center rounded border p-2 text-center">
<FuzzyMatchAlgorithmLabel fuzzyMatchAlgorithm={algorithm} />
</div>
<EvaluationErrors
errors={adaptEvaluationErrorViewModels(errors).map(
getNodeEvaluationErrorMessage,
)}
/>
</div>
);
}

function FuzzyMatchAlgorithmLabel({
fuzzyMatchAlgorithm,
}: {
fuzzyMatchAlgorithm: FuzzyMatchAlgorithm;
}) {
const { t } = useTranslation(['common', 'scenarios']);
return (
<span className="text-s text-grey-100 font-semibold">
{getFuzzyMatchAlgorithmName(t, fuzzyMatchAlgorithm)}
</span>
);
}
Loading

0 comments on commit 1da5924

Please sign in to comment.