diff --git a/grai-frontend/src/components/filters/FilterRow.tsx b/grai-frontend/src/components/filters/FilterRow.tsx index 2fa28a54e..26607f956 100644 --- a/grai-frontend/src/components/filters/FilterRow.tsx +++ b/grai-frontend/src/components/filters/FilterRow.tsx @@ -1,7 +1,8 @@ import React from "react" import { Close } from "@mui/icons-material" -import { Grid, IconButton, TextField } from "@mui/material" +import { Grid, IconButton } from "@mui/material" import FilterField from "./FilterField" +import FilterRowValue from "./FilterRowValue" import { Field, Filter, @@ -84,25 +85,11 @@ const FilterRow: React.FC = ({ /> - {operator?.valueComponent ? ( - operator.valueComponent( - !operator, - filter.value, - (value: string | string[] | null) => onChange({ ...filter, value }), - ) - ) : ( - - onChange({ ...filter, value: event.target.value }) - } - inputProps={{ - "data-testid": "value", - }} - /> - )} + diff --git a/grai-frontend/src/components/filters/FilterRowValue.tsx b/grai-frontend/src/components/filters/FilterRowValue.tsx new file mode 100644 index 000000000..bb1e118e5 --- /dev/null +++ b/grai-frontend/src/components/filters/FilterRowValue.tsx @@ -0,0 +1,105 @@ +import React from "react" +import { CheckBox, CheckBoxOutlineBlank } from "@mui/icons-material" +import { + Autocomplete, + AutocompleteChangeReason, + Checkbox, + TextField, +} from "@mui/material" +import arrayFirst from "helpers/arrayFirst" +import arrayWrap from "helpers/arrayWrap" +import notEmpty from "helpers/notEmpty" +import { Filter, OperationOption, Operator } from "./filters" + +const icon = +const checkedIcon = + +type FilterRowValueProps = { + operator: Operator | null + filter: Filter + onChange: (filter: Filter) => void +} + +const FilterRowValue: React.FC = ({ + operator, + filter, + onChange, +}) => { + const handleValueChange = ( + event: React.SyntheticEvent, + newValue: + | null + | string + | OperationOption + | (null | string | OperationOption)[], + reason: AutocompleteChangeReason, + ) => + onChange({ + ...filter, + value: Array.isArray(newValue) + ? newValue + .map(option => + typeof option === "string" ? option : option?.value, + ) + .filter(notEmpty) + : (typeof newValue === "string" ? newValue : newValue?.value) ?? null, + }) + + if (!operator?.options) + return ( + onChange({ ...filter, value: event.target.value })} + inputProps={{ + "data-testid": "value", + }} + /> + ) + + if (operator.multiple) + return ( + + multiple + openOnFocus + autoSelect + limitTags={1} + options={operator.options} + value={operator.options.filter(option => + arrayWrap(filter.value).includes( + typeof option === "string" ? option : option?.value, + ), + )} + onChange={handleValueChange} + renderInput={params => } + renderOption={(props, option, { selected }) => ( +
  • + + {typeof option === "string" ? option : option?.label} +
  • + )} + data-testid="autocomplete-value" + /> + ) + + return ( + + openOnFocus + autoSelect + disabled={!operator} + options={operator.options} + value={arrayFirst(filter.value)} + onChange={handleValueChange} + renderInput={params => } + data-testid="autocomplete-value" + /> + ) +} + +export default FilterRowValue diff --git a/grai-frontend/src/components/filters/filters.ts b/grai-frontend/src/components/filters/filters.ts new file mode 100644 index 000000000..6c64b5780 --- /dev/null +++ b/grai-frontend/src/components/filters/filters.ts @@ -0,0 +1,203 @@ +export type Filter = { + type: string | null + field: string | null + operator: string | null + value: string | string[] | null +} + +export interface OperationOption { + value: string + label: string +} + +export type Operator = { + value: string + label: string + shortLabel?: string + options?: (string | OperationOption)[] + multiple?: boolean +} + +export type Field = { + value: string + label: string + disabled?: boolean + operators: Operator[] +} + +export type Property = { + value: string + label: string + disabled?: boolean + fields: Field[] +} + +export interface Source { + id: string + name: string +} + +export const defaultFilter: Filter = { + type: "table", + field: null, + operator: null, + value: null, +} + +const nameField: Field = { + value: "name", + label: "Name", + operators: [ + { + value: "equals", + label: "Equals", + shortLabel: "=", + }, + { + value: "contains", + label: "Contains", + shortLabel: "*a*", + }, + { + value: "starts-with", + label: "Starts With", + shortLabel: "a*", + }, + { + value: "ends-with", + label: "Ends With", + shortLabel: "*a", + }, + { + value: "not-equals", + label: "Not Equals", + shortLabel: "!=", + }, + { + value: "not-contains", + label: "Not Contains", + shortLabel: "!*a*", + }, + ], +} + +const namespaceField = (namespaces: string[]): Field => ({ + value: "namespace", + label: "Namespace", + operators: [ + { + value: "equals", + label: "Equals", + options: namespaces, + }, + { + value: "in", + label: "In", + options: namespaces, + multiple: true, + }, + ], +}) + +const sourceField = (sources: Source[]): Field => ({ + value: "data-source", + label: "Data Source", + operators: [ + { + value: "in", + label: "In", + options: sources.map(source => ({ + value: source.id, + label: source.name, + })), + multiple: true, + }, + { + value: "not-in", + label: "Not In", + options: sources.map(source => ({ + value: source.id, + label: source.name, + })), + multiple: true, + }, + ], +}) + +const tagField = (tags: string[]): Field => ({ + value: "tag", + label: "Tag", + operators: [ + { + value: "contains", + label: "Contains", + options: tags, + }, + { + value: "not-contains", + label: "Doesn't Contain", + options: tags, + }, + ], +}) + +export const getProperties = ( + namespaces: string[], + tags: string[], + sources: Source[], +): Property[] => [ + { + value: "table", + label: "Default", + fields: [ + nameField, + namespaceField(namespaces), + sourceField(sources), + tagField(tags), + ], + }, + { + value: "parent", + label: "Has Parent", + disabled: true, + fields: [], + }, + { + value: "no-parent", + label: "No Parent", + disabled: true, + fields: [], + }, + { + value: "child", + label: "Has Child", + disabled: true, + fields: [], + }, + { + value: "no-child", + label: "No Child", + disabled: true, + fields: [], + }, + { + value: "ancestor", + label: "Has Ancestor", + fields: [tagField(tags)], + }, + { + value: "no-ancestor", + label: "No Ancestor", + fields: [tagField(tags)], + }, + { + value: "descendant", + label: "Has Descendant", + fields: [tagField(tags)], + }, + { + value: "no-descendant", + label: "No Descendant", + fields: [tagField(tags)], + }, +] diff --git a/grai-frontend/src/components/filters/filters.tsx b/grai-frontend/src/components/filters/filters.tsx deleted file mode 100644 index 02abb427c..000000000 --- a/grai-frontend/src/components/filters/filters.tsx +++ /dev/null @@ -1,364 +0,0 @@ -import { - CheckBoxOutlineBlank, - CheckBox as CheckBoxIcon, -} from "@mui/icons-material" -import { Autocomplete, Checkbox, TextField } from "@mui/material" -import arrayFirst from "helpers/arrayFirst" -import { arrayWrapDefault } from "helpers/arrayWrap" - -export type Filter = { - type: string | null - field: string | null - operator: string | null - value: string | string[] | null -} - -export interface OperationOption { - value: string - label: string -} - -export type Operator = { - value: string - label: string - shortLabel?: string - valueComponent?: ( - disabled: boolean, - value: string | string[] | null, - onChange: (value: string | string[] | null) => void, - ) => React.ReactNode - options?: (string | OperationOption)[] - multiple?: boolean -} - -export type Field = { - value: string - label: string - disabled?: boolean - operators: Operator[] -} - -export type Property = { - value: string - label: string - disabled?: boolean - fields: Field[] -} - -export interface Source { - id: string - name: string -} - -const icon = -const checkedIcon = - -export const defaultFilter: Filter = { - type: "table", - field: null, - operator: null, - value: null, -} - -const nameField: Field = { - value: "name", - label: "Name", - operators: [ - { - value: "equals", - label: "Equals", - shortLabel: "=", - }, - { - value: "contains", - label: "Contains", - shortLabel: "*a*", - }, - { - value: "starts-with", - label: "Starts With", - shortLabel: "a*", - }, - { - value: "ends-with", - label: "Ends With", - shortLabel: "*a", - }, - { - value: "not-equals", - label: "Not Equals", - shortLabel: "!=", - }, - { - value: "not-contains", - label: "Not Contains", - shortLabel: "!*a*", - }, - ], -} - -const namespaceField = (namespaces: string[]): Field => ({ - value: "namespace", - label: "Namespace", - operators: [ - { - value: "equals", - label: "Equals", - valueComponent: ( - disabled: boolean, - value: string | string[] | null, - onChange: (value: string | string[] | null) => void, - ) => ( - onChange(newValue)} - renderInput={params => } - data-testid="autocomplete-value" - /> - ), - options: namespaces, - }, - { - value: "in", - label: "In", - valueComponent: ( - disabled: boolean, - value: string | string[] | null, - onChange: (value: string | string[] | null) => void, - ) => ( - - multiple - openOnFocus - autoSelect - limitTags={1} - disabled={disabled} - options={namespaces} - value={arrayWrapDefault(value)} - onChange={(event, newValue) => onChange(newValue)} - renderInput={params => } - renderOption={(props, option, { selected }) => ( -
  • - - {option} -
  • - )} - data-testid="autocomplete-value" - /> - ), - options: namespaces, - multiple: true, - }, - ], -}) - -const sourceField = (sources: Source[]): Field => ({ - value: "data-source", - label: "Data Source", - operators: [ - { - value: "in", - label: "In", - valueComponent: ( - disabled: boolean, - value: string | string[] | null, - onChange: (value: string | string[] | null) => void, - ) => ( - - multiple - openOnFocus - autoSelect - limitTags={1} - disabled={disabled} - options={sources} - value={sources.filter(source => - arrayWrapDefault(value).includes(source.id), - )} - getOptionLabel={source => source.name} - onChange={(event, newValue) => - onChange(newValue.map(source => source.id)) - } - renderInput={params => } - renderOption={(props, option, { selected }) => ( -
  • - - {option.name} -
  • - )} - data-testid="autocomplete-value" - /> - ), - options: sources.map(source => ({ - value: source.id, - label: source.name, - })), - multiple: true, - }, - { - value: "not-in", - label: "Not In", - valueComponent: ( - disabled: boolean, - value: string | string[] | null, - onChange: (value: string | string[] | null) => void, - ) => ( - - multiple - openOnFocus - autoSelect - limitTags={1} - disabled={disabled} - options={sources} - value={sources.filter(source => - arrayWrapDefault(value).includes(source.id), - )} - getOptionLabel={source => source.name} - onChange={(event, newValue) => - onChange(newValue.map(source => source.id)) - } - renderInput={params => } - renderOption={(props, option, { selected }) => ( -
  • - - {option.name} -
  • - )} - data-testid="autocomplete-value" - /> - ), - options: sources.map(source => ({ - value: source.id, - label: source.name, - })), - multiple: true, - }, - ], -}) - -const tagField = (tags: string[]): Field => ({ - value: "tag", - label: "Tag", - operators: [ - { - value: "contains", - label: "Contains", - valueComponent: ( - disabled: boolean, - value: string | string[] | null, - onChange: (value: string | string[] | null) => void, - ) => ( - onChange(newValue)} - renderInput={params => } - data-testid="autocomplete-value" - /> - ), - options: tags, - }, - { - value: "not-contains", - label: "Doesn't Contain", - valueComponent: ( - disabled: boolean, - value: string | string[] | null, - onChange: (value: string | string[] | null) => void, - ) => ( - onChange(newValue)} - renderInput={params => } - data-testid="autocomplete-value" - /> - ), - options: tags, - }, - ], -}) - -export const getProperties = ( - namespaces: string[], - tags: string[], - sources: Source[], -): Property[] => [ - { - value: "table", - label: "Default", - fields: [ - nameField, - namespaceField(namespaces), - sourceField(sources), - tagField(tags), - ], - }, - { - value: "parent", - label: "Has Parent", - disabled: true, - fields: [], - }, - { - value: "no-parent", - label: "No Parent", - disabled: true, - fields: [], - }, - { - value: "child", - label: "Has Child", - disabled: true, - fields: [], - }, - { - value: "no-child", - label: "No Child", - disabled: true, - fields: [], - }, - { - value: "ancestor", - label: "Has Ancestor", - fields: [tagField(tags)], - }, - { - value: "no-ancestor", - label: "No Ancestor", - fields: [tagField(tags)], - }, - { - value: "descendant", - label: "Has Descendant", - fields: [tagField(tags)], - }, - { - value: "no-descendant", - label: "No Descendant", - fields: [tagField(tags)], - }, -] diff --git a/grai-frontend/src/helpers/arrayWrap.ts b/grai-frontend/src/helpers/arrayWrap.ts index da2dbffff..cd86244c6 100644 --- a/grai-frontend/src/helpers/arrayWrap.ts +++ b/grai-frontend/src/helpers/arrayWrap.ts @@ -1,9 +1,4 @@ const arrayWrap = (value: T | T[]): T[] => Array.isArray(value) ? value : [value] -export const arrayWrapDefault = ( - value: T | NonNullable[] | undefined, - defaultValue: NonNullable[] = [], -): NonNullable[] => (value ? arrayWrap(value) : defaultValue) - export default arrayWrap