Skip to content

Commit

Permalink
feat(app-aco): support reference field in advanced search filter (#3783)
Browse files Browse the repository at this point in the history
  • Loading branch information
leopuleo authored Mar 18, 2024
1 parent 65cde0a commit 0e0841f
Show file tree
Hide file tree
Showing 51 changed files with 1,183 additions and 373 deletions.
23 changes: 19 additions & 4 deletions packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo } from "react";
import { observer } from "mobx-react-lite";

import { FieldRaw, FilterDTO, FilterRepository } from "./domain";
import { Field, FieldMapper, FieldRaw, FilterDTO, FilterRepository } from "./domain";

import { AdvancedSearchPresenter } from "./AdvancedSearchPresenter";

Expand All @@ -13,18 +13,21 @@ import { QuerySaverDialog } from "./QuerySaverDialog";
import { SelectedFilter } from "./SelectedFilter";

import { AdvancedSearchContainer } from "./AdvancedSearch.styled";
import { useAcoConfig } from "~/config";

interface AdvancedSearchProps {
export interface AdvancedSearchProps {
fields: FieldRaw[];
repository: FilterRepository;
onApplyFilter: (data: FilterDTO | null) => void;
}

export const AdvancedSearch = observer(
({ fields, repository, onApplyFilter }: AdvancedSearchProps) => {
const { advancedSearch } = useAcoConfig();

const presenter = useMemo<AdvancedSearchPresenter>(() => {
return new AdvancedSearchPresenter(repository);
}, [FilterRepository]);
}, [repository]);

useEffect(() => {
presenter.load();
Expand Down Expand Up @@ -56,6 +59,18 @@ export const AdvancedSearch = observer(
onApplyFilter(filter);
};

const fieldsWithRenderer = useMemo(() => {
const fieldDTOs = FieldMapper.toDTO(fields.map(field => Field.createFromRaw(field)));

return fieldDTOs.map(field => {
const config = advancedSearch.fieldRenderers.find(
config => config.type === field.type
);
const element = config?.element ?? null;
return { ...field, element };
});
}, [fields, advancedSearch.fieldRenderers]);

return (
<>
<AdvancedSearchContainer>
Expand All @@ -81,7 +96,7 @@ export const AdvancedSearch = observer(
{presenter.vm.currentFilter ? (
<>
<QueryBuilderDrawer
fields={fields}
fields={fieldsWithRenderer}
onClose={() => presenter.closeBuilder()}
onSave={filter => presenter.saveFilter(filter)}
onApply={applyFilter}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react";
import { AcoConfig } from "~/config";
import { FieldType } from "~/components/AdvancedSearch/domain";
import {
Boolean,
DateWithoutTimezone,
DateWithTimezone,
Input,
MultipleValues
} from "~/components/AdvancedSearch/fields";

const { AdvancedSearch } = AcoConfig;

export const AdvancedSearchConfigs = () => {
return (
<AcoConfig>
<AdvancedSearch.FieldRenderer name={"text"} type={FieldType.TEXT} element={<Input />} />
<AdvancedSearch.FieldRenderer name={"date"} type={FieldType.DATE} element={<Input />} />
<AdvancedSearch.FieldRenderer name={"time"} type={FieldType.TIME} element={<Input />} />
<AdvancedSearch.FieldRenderer
name={"number"}
type={FieldType.NUMBER}
element={<Input />}
/>
<AdvancedSearch.FieldRenderer
name={"boolean"}
type={FieldType.BOOLEAN}
element={<Boolean />}
/>
<AdvancedSearch.FieldRenderer
name={"dateTimeWithTimezone"}
type={FieldType.DATETIME_WITH_TIMEZONE}
element={<DateWithTimezone />}
/>
<AdvancedSearch.FieldRenderer
name={"dateTimeWithoutTimezone"}
type={FieldType.DATETIME_WITHOUT_TIMEZONE}
element={<DateWithoutTimezone />}
/>
<AdvancedSearch.FieldRenderer
name={"multipleValues"}
type={FieldType.MULTIPLE_VALUES}
element={<MultipleValues />}
/>
</AcoConfig>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,94 @@ describe("GraphQLInputMapper", () => {
]
});
});

it("should return a GraphQL formatted output and support nested objects", () => {
const filterDTO: FilterDTO = {
id: "any-id",
name: "Untitled",
operation: Operation.AND,
groups: [
{
operation: Operation.AND,
filters: [
{
field: "field-1",
condition: " ",
value: "value-1"
},
{
field: "field-2",
condition: "_not",
value: "value-2"
},
{
field: "field-3.entryId",
condition: " ",
value: JSON.stringify({ entryId: "value-3", modelId: "any-modelId" })
},
{
field: "field-4.entryId",
condition: "_not",
value: JSON.stringify({ entryId: "value-4", modelId: "any-modelId" })
},
{
field: "field-5.sub-field.entryId",
condition: " ",
value: JSON.stringify({
"sub-field": { entryId: "value-5", modelId: "any-modelId" }
})
},
{
field: "field-6.sub-field.entryId",
condition: "_not",
value: JSON.stringify({
"sub-field": { entryId: "value-6", modelId: "any-modelId" }
})
}
]
}
]
};

const output = GraphQLInputMapper.toGraphQL(filterDTO);

expect(output).toEqual({
[Operation.AND]: [
{
[Operation.AND]: [
{
"field-1": "value-1"
},
{
"field-2_not": "value-2"
},
{
"field-3": {
entryId: "value-3"
}
},
{
"field-4": {
entryId_not: "value-4"
}
},
{
"field-5": {
"sub-field": {
entryId: "value-5"
}
}
},
{
"field-6": {
"sub-field": {
entryId_not: "value-6"
}
}
}
]
}
]
});
});
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
import get from "lodash/get";
import { FilterDTO } from "~/components/AdvancedSearch/domain";

interface NestedObject {
[key: string]: string | boolean | NestedObject;
}

export class GraphQLInputMapper {
static toGraphQL(configuration: FilterDTO) {
return {
[configuration.operation]: configuration.groups.map(group => {
return {
[group.operation]: group.filters.map(filter => {
const { field, condition, value } = filter;
const key = `${field}${condition}`.trim();

return { [key]: this.convertToBooleanOrString(value) };
const keys = field.trim().split(".");
keys.shift();

if (keys.length === 0) {
return this.createNestedObject(
this.createKeys(field, condition),
this.convertToBooleanOrString(value)
);
}

try {
const values = JSON.parse(value);
return this.createNestedObject(
this.createKeys(field, condition),
this.convertToBooleanOrString(get(values, keys))
);
} catch {
return this.createNestedObject(
this.createKeys(field, condition),
this.convertToBooleanOrString(value)
);
}
})
};
})
Expand All @@ -27,4 +52,12 @@ export class GraphQLInputMapper {

return value ?? "";
}

private static createKeys(field: string, condition: string): string[] {
return `${field}${condition}`.trim().split(".");
}

private static createNestedObject(keys: string[], value: string | boolean): NestedObject {
return keys.reduceRight((acc, key) => ({ [key]: acc }), value as unknown as NestedObject);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { Fragment, useEffect } from "react";

import { observer } from "mobx-react-lite";
import { ReactComponent as DeleteIcon } from "@material-design-icons/svg/outlined/delete_outline.svg";

Expand All @@ -19,6 +18,7 @@ import {
} from "./components";

import { AccordionItemInner, Content, FilterOperationContainer } from "./Querybuilder.styled";
import { FieldDTOWithElement } from "~/components/AdvancedSearch/domain";

export interface QueryBuilderProps {
onForm: (form: FormAPI) => void;
Expand All @@ -29,6 +29,7 @@ export interface QueryBuilderProps {
onDeleteFilterFromGroup: (groupIndex: number, filterIndex: number) => void;
onAddNewFilterToGroup: (groupIndex: number) => void;
onAddGroup: () => void;
fields: FieldDTOWithElement[];
vm: QueryBuilderViewModel;
}

Expand Down Expand Up @@ -84,7 +85,7 @@ export const QueryBuilder = observer((props: QueryBuilderProps) => {
<Filter
name={`groups.${groupIndex}.filters.${filterIndex}`}
filter={filter}
fields={props.vm.fields}
fields={props.fields}
onFieldSelectChange={data =>
props.onSetFilterFieldData(
groupIndex,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from "react";

import { Bind } from "@webiny/form";
import { AutoComplete } from "@webiny/ui/AutoComplete";
import { Cell, Grid } from "@webiny/ui/Grid";
Expand All @@ -8,14 +7,14 @@ import { Select } from "@webiny/ui/Select";
import { InputField } from "./InputField";
import { RemoveFilter } from "./controls";

import { FieldDTO, FilterGroupFilterDTO } from "../../../domain";
import { FieldDTOWithElement, FilterGroupFilterDTO } from "../../../domain";

import { CellInner, FilterContainer } from "../Querybuilder.styled";

interface FilterProps {
name: string;
filter: FilterGroupFilterDTO & { canDelete: boolean };
fields: FieldDTO[];
fields: FieldDTOWithElement[];
onDelete: () => void;
onFieldSelectChange: (data: string) => void;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from "react";

import { Boolean, DateWithoutTimezone, DateWithTimezone, Input, MultipleValues } from "./fields";

import { FieldDTO, FieldType } from "~/components/AdvancedSearch/domain";
import { Typography } from "@webiny/ui/Typography";
import { InputFieldProvider } from "~/components";
import { FieldDTOWithElement } from "~/components/AdvancedSearch/domain";

interface InputFieldProps {
field?: FieldDTO;
field?: FieldDTOWithElement;
name: string;
}

Expand All @@ -14,16 +14,19 @@ export const InputField = ({ field, name }: InputFieldProps) => {
return null;
}

switch (field.type) {
case FieldType.BOOLEAN:
return <Boolean name={name} />;
case FieldType.DATETIME_WITH_TIMEZONE:
return <DateWithTimezone name={name} />;
case FieldType.DATETIME_WITHOUT_TIMEZONE:
return <DateWithoutTimezone name={name} />;
case FieldType.MULTIPLE_VALUES:
return <MultipleValues predefined={field.predefined} name={name} />;
default:
return <Input name={name} type={field.type} />;
const { element, ...rest } = field;

if (!element) {
return (
<Typography
use={"body2"}
>{`Cannot render "${field.type}" field: missing field renderer.`}</Typography>
);
}

return (
<InputFieldProvider field={rest} name={name}>
{element}
</InputFieldProvider>
);
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from "./controls";
export * from "./fields";
export * from "./Details";
export * from "./Filter";
export * from "./InputField";
Expand Down
Loading

0 comments on commit 0e0841f

Please sign in to comment.