Skip to content

Commit

Permalink
feat: add aggregation filter operators
Browse files Browse the repository at this point in the history
  • Loading branch information
ChibiBlasphem committed Jan 16, 2025
1 parent 1964d56 commit beea280
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import {
type AggregationAstNode,
aggregationAstNodeName,
type AstNode,
type BinaryFilterOperator,
binaryFilterOperators,
type FilterAstNode,
type FilterOperator,
type GetFilterOperator,
isUnaryFilter,
NewConstantAstNode,
type UnaryFilterOperator,
unaryFilterOperators,
} from '@app-builder/models';
import {
type AggregatorOperator,
Expand Down Expand Up @@ -47,17 +55,42 @@ export interface AggregationViewModel {
aggregatedField: EvaluationError[];
};
}
export interface FilterViewModel {
operator: string | null;
export type FilterViewModel<T extends FilterOperator = FilterOperator> = {
operator: T | null;
filteredField: DataModelField | null;
value: { astNode: AstNode; astNodeErrors?: AstNodeErrors };
errors: {
filter: EvaluationError[];
operator: EvaluationError[];
filteredField: EvaluationError[];
value: EvaluationError[];
};
}
} & (T extends UnaryFilterOperator
? { value?: undefined }
: {
value: { astNode: AstNode; astNodeErrors?: AstNodeErrors };
});

export const isUnaryFilterModel = (
filter: FilterViewModel,
): filter is FilterViewModel<UnaryFilterOperator> => {
return (
!!filter.operator &&
(unaryFilterOperators as ReadonlyArray<FilterOperator>).includes(
filter.operator,
)
);
};

export const isBinaryFilterModel = (
filter: FilterViewModel,
): filter is FilterViewModel<BinaryFilterOperator> => {
return (
filter.operator === null ||
(binaryFilterOperators as ReadonlyArray<FilterOperator>).includes(
filter.operator,
)
);
};

export const adaptAggregationViewModel = (
initialAggregationAstNode: AggregationAstNode,
Expand Down Expand Up @@ -114,20 +147,24 @@ export const adaptAggregationViewModel = (
};
};

function adaptFilterViewModel(
filterAstNode: AggregationAstNode['namedChildren']['filters']['children'][number],
function adaptFilterViewModel<T extends FilterAstNode = FilterAstNode>(
filterAstNode: T,
initialAstNodeErrors: AstNodeErrors,
): FilterViewModel {
): FilterViewModel<GetFilterOperator<T>> {
return {
operator: filterAstNode.namedChildren.operator.constant,
filteredField: {
tableName: filterAstNode.namedChildren.tableName?.constant,
fieldName: filterAstNode.namedChildren.fieldName?.constant,
},
value: {
astNode: filterAstNode.namedChildren.value,
astNodeErrors: initialAstNodeErrors.namedChildren['value'],
},
...(isUnaryFilter(filterAstNode)
? {}
: {
value: {
astNode: filterAstNode.namedChildren.value,
astNodeErrors: initialAstNodeErrors.namedChildren['value'],
},
}),
errors: {
filter: initialAstNodeErrors.errors,
operator: computeValidationForNamedChildren(
Expand All @@ -140,36 +177,41 @@ function adaptFilterViewModel(
initialAstNodeErrors,
['tableName', 'fieldName'],
),
value: computeValidationForNamedChildren(
filterAstNode,
initialAstNodeErrors,
'value',
),
...(isUnaryFilter(filterAstNode)
? {}
: {
value: computeValidationForNamedChildren(
filterAstNode,
initialAstNodeErrors,
'value',
),
}),
},
};
} as FilterViewModel<GetFilterOperator<T>>;
}

export const adaptAggregationAstNode = (
aggregationViewModel: AggregationViewModel,
): AggregationAstNode => {
const filters = aggregationViewModel.filters.map(
(filter: FilterViewModel) => ({
name: 'Filter' as const,
constant: undefined,
children: [],
namedChildren: {
operator: NewConstantAstNode({
constant: filter.operator,
}),
tableName: NewConstantAstNode({
constant: filter.filteredField?.tableName ?? null,
}),
fieldName: NewConstantAstNode({
constant: filter.filteredField?.fieldName ?? null,
}),
value: filter.value.astNode,
},
}),
(filter: FilterViewModel) =>
({
name: 'Filter' as const,
constant: undefined,
children: [],
namedChildren: {
operator: NewConstantAstNode({
constant: filter.operator,
}),
tableName: NewConstantAstNode({
constant: filter.filteredField?.tableName ?? null,
}),
fieldName: NewConstantAstNode({
constant: filter.filteredField?.fieldName ?? null,
}),
value: filter.value?.astNode,
},
}) as FilterAstNode,
);
return {
name: aggregationAstNodeName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { scenarioI18n } from '@app-builder/components/Scenario';
import { RemoveButton } from '@app-builder/components/Scenario/AstBuilder/RemoveButton';
import { LogicalOperatorLabel } from '@app-builder/components/Scenario/AstBuilder/RootAstBuilderNode/LogicalOperator';
import { EvaluationErrors } from '@app-builder/components/Scenario/ScenarioValidationError';
import { type AstNode, NewUndefinedAstNode } from '@app-builder/models';
import {
type AstNode,
filterOperators,
isFilterOperator,
} from '@app-builder/models/editable-operators';
isUnaryFilterOperator,
NewUndefinedAstNode,
} from '@app-builder/models';
import {
useDefaultCoerceToConstant,
useGetAstNodeOperandProps,
Expand All @@ -30,7 +32,7 @@ import { Icon } from 'ui-icons';

import { Operator } from '../../../../Operator';
import { Operand } from '../../../Operand';
import { type FilterViewModel } from './AggregationEdit';
import { type FilterViewModel, isBinaryFilterModel } from './AggregationEdit';
import { type DataModelField, EditDataModelField } from './EditDataModelField';

const newFilterValidation = () => ({
Expand Down Expand Up @@ -68,15 +70,42 @@ export function EditFilters({
filterIndex: number,
): void => {
onChange(
value.map((filter, index) =>
index === filterIndex
? {
...filter,
...newFieldValue,
validation: newFilterValidation(),
value.map((filter, index) => {
if (index !== filterIndex) {
return filter;
}

if ('operator' in newFieldValue && !!newFieldValue.operator) {
const isOldOpUnary = isUnaryFilterOperator(filter.operator);
const isNewOpUnary = isUnaryFilterOperator(newFieldValue.operator);
const isBinaryToUnary = !isOldOpUnary && isNewOpUnary;
const isUnaryToBinary = isOldOpUnary && !isNewOpUnary;

if (isBinaryToUnary || isUnaryToBinary) {
if (isBinaryToUnary) {
return {
operator: newFieldValue.operator,
filteredField: filter.filteredField,
value: undefined,
errors: newFilterValidation(),
};
} else {
return {
operator: newFieldValue.operator,
filteredField: filter.filteredField,
value: { astNode: NewUndefinedAstNode() },
errors: newFilterValidation(),
};
}
: filter,
),
}
}

return {
...filter,
...newFieldValue,
errors: newFilterValidation(),
};
}),
);
};

Expand Down Expand Up @@ -104,6 +133,8 @@ export function EditFilters({
{value.map((filter, filterIndex) => {
const isFirstCondition = filterIndex === 0;
const isLastCondition = filterIndex === value.length - 1;
const binaryFilter = isBinaryFilterModel(filter);

return (
<Fragment key={filterIndex}>
{/* Row 1 */}
Expand Down Expand Up @@ -151,21 +182,23 @@ export function EditFilters({
}
operators={filterOperators}
/>
<FilterValue
filterValue={filter.value.astNode}
onSave={(astNode) =>
onFilterChange({ value: { astNode } }, filterIndex)
}
astNodeErrors={filter.value.astNodeErrors}
validationStatus={
filter.errors.value.length > 0 ? 'error' : 'valid'
}
/>
{binaryFilter ? (
<FilterValue
filterValue={filter.value.astNode}
onSave={(astNode) =>
onFilterChange({ value: { astNode } }, filterIndex)
}
astNodeErrors={filter.value.astNodeErrors}
validationStatus={
filter.errors.value.length > 0 ? 'error' : 'valid'
}
/>
) : null}
</div>
<EvaluationErrors
errors={adaptEvaluationErrorViewModels([
...filter.errors.filter,
...filter.errors.value,
...(isBinaryFilterModel(filter) ? filter.errors.value : []),
]).map(getNodeEvaluationErrorMessage)}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getDataTypeIcon,
getDataTypeTKey,
isAggregation,
isBinaryFilter,
isCustomListAccess,
isDataAccessorAstNode,
isTimeAdd,
Expand Down Expand Up @@ -219,7 +220,7 @@ function AggregatorDescription({ astNode }: { astNode: AggregationAstNode }) {
</span>
<span className="font-bold">{aggregatedFieldName}</span>
{filters.children.map((filter, index) => {
const { operator, fieldName, value } = filter.namedChildren;
const { operator, fieldName } = filter.namedChildren;
return (
<Fragment key={`filter_${index}`}>
<LogicalOperatorLabel
Expand All @@ -244,10 +245,12 @@ function AggregatorDescription({ astNode }: { astNode: AggregationAstNode }) {
operators={[operator?.constant as OperatorFunction]}
viewOnly
/>
<OperandLabel
interactionMode="viewer"
{...getAstNodeOperandProps(value)}
/>
{isBinaryFilter(filter) ? (
<OperandLabel
interactionMode="viewer"
{...getAstNodeOperandProps(filter.namedChildren.value)}
/>
) : null}
</div>
</Fragment>
);
Expand Down
Loading

0 comments on commit beea280

Please sign in to comment.