Browse Source

init commit

czhen 4 years ago
parent
commit
4a66ac380a

+ 271 - 0
client/src/components/advancedSearch/Condition.tsx

@@ -0,0 +1,271 @@
+import React, { useState, useEffect, FC } from 'react';
+import PropTypes from 'prop-types';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  IconButton,
+  TextField,
+  MenuItem,
+} from '@material-ui/core';
+import CloseIcon from '@material-ui/icons/Close';
+
+interface ConditionProps {
+  others?: object;
+  onDelete: () => void;
+  triggerChange: (data: TriggerChangeData) => void;
+  fields: Field[];
+  id: string;
+  initData: any;
+  className?: string;
+}
+
+interface Field {
+  name: string;
+  type: 'int' | 'float';
+}
+
+interface TriggerChangeData {
+  field: Field;
+  op: string;
+  value: string;
+  isCorrect: boolean;
+  id: string;
+}
+
+// Static logical operators.
+const LogicalOperators = [
+  {
+    value: '<',
+    label: '<',
+  },
+  {
+    value: '<=',
+    label: '<=',
+  },
+  {
+    value: '>',
+    label: '>',
+  },
+  {
+    value: '>=',
+    label: '>=',
+  },
+  {
+    value: '==',
+    label: '==',
+  },
+  {
+    value: '!=',
+    label: '!=',
+  },
+  {
+    value: 'in',
+    label: 'in',
+  },
+];
+
+const Condition: FC<ConditionProps> = props => {
+  const {
+    onDelete,
+    triggerChange,
+    fields = [],
+    id,
+    initData,
+    className,
+    ...others
+  } = props;
+  const [operator, setOperator] = useState(
+    initData?.op || LogicalOperators[0].value
+  );
+  const [conditionField, setConditionField] = useState<Field | any>(
+    initData?.field || fields[0] || {}
+  );
+  const [conditionValue, setConditionValue] = useState(initData?.value || '');
+  const [isValuelegal, setIsValueLegal] = useState(
+    initData?.isCorrect || false
+  );
+
+  /**
+   * Check condition's value by field's and operator's type.
+   * Trigger condition change event.
+   */
+  useEffect(() => {
+    const regInt = /^\d+$/;
+    const regFloat = /^\d+\.\d+$/;
+    const regIntInterval = /^\[\d+,\d+\]$/;
+    const regFloatInterval = /^\[\d+\.\d+,\d+\.\d+]$/;
+
+    const type = conditionField?.type;
+    const isIn = operator === 'in';
+
+    switch (type) {
+      case 'int':
+        setIsValueLegal(
+          isIn
+            ? regIntInterval.test(conditionValue)
+            : regInt.test(conditionValue)
+        );
+        break;
+      case 'float':
+        setIsValueLegal(
+          isIn
+            ? regFloatInterval.test(conditionValue)
+            : regFloat.test(conditionValue)
+        );
+        break;
+      default:
+        setIsValueLegal(false);
+        break;
+    }
+
+    triggerChange({
+      field: conditionField,
+      op: operator,
+      value: conditionValue,
+      isCorrect: isValuelegal,
+      id,
+    });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [conditionField, operator, conditionValue]);
+
+  // Trigger change event if isValuelegal changed.
+  useEffect(() => {
+    triggerChange({
+      field: conditionField,
+      op: operator,
+      value: conditionValue,
+      isCorrect: isValuelegal,
+      id,
+    });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [isValuelegal]);
+
+  const classes = useStyles();
+
+  // Logic operator input change.
+  const handleOpChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setOperator(event.target.value);
+  };
+  // Field Name input change.
+  const handleFieldNameChange = (
+    event: React.ChangeEvent<HTMLInputElement>
+  ) => {
+    const value = event.target.value;
+    const target = fields.find(field => field.name === value);
+    target && setConditionField(target);
+  };
+  // Value input change.
+  const handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const value = event.target.value;
+    setConditionValue(value);
+  };
+
+  return (
+    <div className={`${classes.wrapper} ${className}`} {...others}>
+      <TextField
+        className={classes.fieldName}
+        label="Field Name"
+        variant="filled"
+        size="small"
+        select
+        onChange={handleFieldNameChange}
+        value={conditionField?.name}
+      >
+        {fields.map((field: Field) => (
+          <MenuItem key={field.name} value={field.name}>
+            {field.name}
+          </MenuItem>
+        ))}
+      </TextField>
+      <TextField
+        className={classes.logic}
+        label="Logic"
+        variant="filled"
+        size="small"
+        select
+        onChange={handleOpChange}
+        defaultValue={LogicalOperators[0].value}
+        value={operator}
+      >
+        {LogicalOperators.map(option => (
+          <MenuItem key={option.value} value={option.value}>
+            {option.label}
+          </MenuItem>
+        ))}
+      </TextField>
+      <TextField
+        className={classes.value}
+        label="Value"
+        variant="filled"
+        size="small"
+        onChange={handleValueChange}
+        value={conditionValue}
+        error={!isValuelegal}
+      />
+      <IconButton
+        aria-label="close"
+        className={classes.closeButton}
+        onClick={onDelete}
+        size="small"
+      >
+        <CloseIcon />
+      </IconButton>
+    </div>
+  );
+};
+
+Condition.propTypes = {
+  /**
+   * Function will be called if delete button is clicked.
+   */
+  onDelete: PropTypes.func.isRequired,
+  /**
+   * Function will be called if input value is changed.
+   */
+  triggerChange: PropTypes.func.isRequired,
+  // fields: PropTypes.arrayOf(
+  //   PropTypes.shape({
+  //     name: PropTypes.string.isRequired,
+  //     type: PropTypes.oneOf(['int', 'float']).isRequired,
+  //   }).isRequired
+  // ).isRequired,
+  id: PropTypes.string.isRequired,
+  className: PropTypes.string,
+  others: PropTypes.object,
+};
+
+Condition.defaultProps = {
+  onDelete: () => {},
+  triggerChange: () => {},
+  fields: [],
+  id: '',
+  className: '',
+};
+
+Condition.displayName = 'Condition';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      minWidth: '466px',
+      minHeight: '62px',
+      background: '#FFFFFF',
+      // borderRadius: '8px',
+      padding: '12px 16px',
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+    },
+    closeButton: {},
+    fieldName: {
+      minHeight: '38px',
+      minWidth: '130px',
+    },
+    logic: { minHeight: '38px', minWidth: '70px', margin: '0 24px' },
+    value: { minHeight: '38px', minWidth: '130px' },
+  })
+);
+
+export default Condition;

+ 263 - 0
client/src/components/advancedSearch/ConditionGroup.tsx

@@ -0,0 +1,263 @@
+import React, { useState, FC } from 'react';
+import PropTypes from 'prop-types';
+import { makeStyles, Theme, createStyles, Button } from '@material-ui/core';
+import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
+import ConditionItem from './Condition';
+import AddIcon from '@material-ui/icons/Add';
+
+interface ConditionGroupProps {
+  others?: object;
+  fields: Field[];
+  handleConditions: any;
+  conditions: any[];
+}
+
+interface BinaryLogicalOpProps {
+  onChange: (newOp: string) => void;
+  className?: string;
+  initValue?: string;
+}
+
+interface AddConditionProps {
+  className?: string;
+  onClick?: () => void;
+}
+
+interface Field {
+  name: string;
+  type: 'int' | 'float';
+}
+
+// "And & or" operator component.
+const BinaryLogicalOp: FC<BinaryLogicalOpProps> = props => {
+  const { onChange, className, initValue = 'and' } = props;
+  const [operator, setOperator] = useState(initValue);
+  const handleChange = (
+    event: React.MouseEvent<HTMLElement>,
+    newOp: string
+  ) => {
+    if (newOp !== null) {
+      setOperator(newOp);
+      onChange(newOp);
+    }
+  };
+  return (
+    <>
+      <div className={`${className} op-${operator}`}>
+        <ToggleButtonGroup
+          value={operator}
+          exclusive
+          onChange={handleChange}
+          aria-label="Binary Logical Operator"
+        >
+          <ToggleButton value="and" aria-label="And">
+            AND
+          </ToggleButton>
+          <ToggleButton value="or" aria-label="Or">
+            OR
+          </ToggleButton>
+        </ToggleButtonGroup>
+        <div className="op-split" />
+      </div>
+    </>
+  );
+};
+
+// "+ Add condition" component.
+const AddCondition: FC<AddConditionProps> = props => {
+  const { className, onClick } = props;
+  return (
+    <Button onClick={onClick} color="primary" className={className}>
+      <AddIcon />
+      Add Condition
+    </Button>
+  );
+};
+
+// Condition group component which contains of BinaryLogicalOp, AddCondition and ConditionItem.
+const ConditionGroup = React.forwardRef((props: ConditionGroupProps, ref) => {
+  const { fields, handleConditions, conditions: flatConditions } = props;
+  const {
+    addCondition,
+    removeCondition,
+    changeBinaryLogicalOp,
+    onConditionChange,
+  } = handleConditions;
+
+  const classes = useStyles();
+
+  const generateClassName = (conditions: any, currentIndex: number) => {
+    let className = '';
+    if (currentIndex === 0 || conditions[currentIndex - 1].type === 'break') {
+      className += 'radius-top';
+    }
+    if (
+      currentIndex === conditions.length - 1 ||
+      conditions[currentIndex + 1].type === 'break'
+    ) {
+      className ? (className = 'radius-all') : (className = 'radius-bottom');
+    }
+    return className;
+  };
+
+  // Generate condition items with operators and add condition btn.
+  const generateConditionItems = (conditions: any[]) => {
+    const conditionsLength = conditions.length;
+    const results = conditions.reduce((prev: any, condition, ind) => {
+      if (ind === conditionsLength - 1) {
+        prev.push(
+          <ConditionItem
+            key={condition.id}
+            id={condition.id}
+            onDelete={() => {
+              removeCondition(condition.id);
+            }}
+            fields={fields}
+            triggerChange={onConditionChange}
+            initData={condition?.data}
+            className={generateClassName(conditions, ind)}
+          />
+        );
+        prev.push(
+          <AddCondition
+            key={`${condition.id}-add`}
+            className={classes.addBtn}
+            onClick={() => {
+              addCondition(condition.id);
+            }}
+          />
+        );
+      } else if (condition.type === 'break') {
+        prev.pop();
+        prev.push(
+          <AddCondition
+            key={`${condition.id}-add`}
+            className={classes.addBtn}
+            onClick={() => {
+              addCondition(condition.id, true);
+            }}
+          />
+        );
+        prev.push(
+          <BinaryLogicalOp
+            key={`${condition.id}-op`}
+            onChange={newOp => {
+              changeBinaryLogicalOp(newOp, condition.id);
+            }}
+            className={classes.binaryLogicOp}
+            initValue="or"
+          />
+        );
+      } else if (condition.type === 'condition') {
+        prev.push(
+          <ConditionItem
+            key={condition.id}
+            id={condition.id}
+            onDelete={() => {
+              removeCondition(condition.id);
+            }}
+            fields={fields}
+            triggerChange={onConditionChange}
+            initData={condition?.data}
+            className={generateClassName(conditions, ind)}
+          />
+        );
+        prev.push(
+          <BinaryLogicalOp
+            key={`${condition.id}-op`}
+            onChange={newOp => {
+              changeBinaryLogicalOp(newOp, condition.id);
+            }}
+            className={classes.binaryLogicOp}
+          />
+        );
+      }
+      return prev;
+    }, []);
+    return results;
+  };
+
+  return (
+    <div className={classes.wrapper}>
+      {generateConditionItems(flatConditions)}
+      {flatConditions?.length === 0 && (
+        <AddCondition
+          className={classes.addBtn}
+          onClick={() => {
+            addCondition();
+          }}
+        />
+      )}
+    </div>
+  );
+});
+
+ConditionGroup.propTypes = {
+  others: PropTypes.object,
+  // fields: PropTypes.arrayOf(PropTypes.shape({
+  //   name: PropTypes.string.isRequired,
+  //   type: PropTypes.oneOf(['int', 'float']).isRequired,
+  // }).isRequired).isRequired,
+  handleConditions: PropTypes.object.isRequired,
+  conditions: PropTypes.array.isRequired,
+};
+
+ConditionGroup.defaultProps = {
+  fields: [],
+};
+
+ConditionGroup.displayName = 'ConditionGroup';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      display: 'flex',
+      flexDirection: 'column',
+      alignItems: 'center',
+
+      '& .op-or': {
+        backgroundColor: 'unset',
+        margin: '16px 0',
+      },
+
+      '& .radius-top': {
+        borderRadius: '8px 8px 0 0',
+      },
+      '& .radius-bottom': {
+        borderRadius: '0 0 8px 8px',
+      },
+      '& .radius-all': {
+        borderRadius: '8px',
+      },
+    },
+    addBtn: {},
+    binaryLogicOp: {
+      width: '100%',
+      backgroundColor: '#FFFFFF',
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+      '& .op-split': {
+        height: '1px',
+        backgroundColor: '#E9E9ED',
+        width: '100%',
+      },
+      '& button': {
+        width: '42px',
+        height: '32px',
+        color: '#010E29',
+      },
+      '& button.Mui-selected': {
+        backgroundColor: '#06AFF2',
+        color: '#FFFFFF',
+      },
+      '& button.Mui-selected:hover': {
+        backgroundColor: '#06AFF2',
+        color: '#FFFFFF',
+      },
+    },
+  })
+);
+
+export default ConditionGroup;

+ 102 - 0
client/src/components/advancedSearch/CopyButton.tsx

@@ -0,0 +1,102 @@
+import React, { useState, FC } from 'react';
+import PropTypes from 'prop-types';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  Tooltip,
+  IconButton,
+  Fade,
+} from '@material-ui/core';
+
+interface CopyButtonProps {
+  className?: string;
+  icon?: any;
+  label: string;
+  value: string;
+  others?: any;
+}
+
+const CopyIcon = (
+  <svg
+    width="16"
+    height="16"
+    viewBox="0 0 16 16"
+    fill="none"
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <path
+      d="M13.3333 6H7.33333C6.59695 6 6 6.59695 6 7.33333V13.3333C6 14.0697 6.59695 14.6667 7.33333 14.6667H13.3333C14.0697 14.6667 14.6667 14.0697 14.6667 13.3333V7.33333C14.6667 6.59695 14.0697 6 13.3333 6Z"
+      stroke="#06AFF2"
+      strokeWidth="2"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+    />
+    <path
+      d="M3.33301 9.99992H2.66634C2.31272 9.99992 1.97358 9.85944 1.72353 9.60939C1.47348 9.35935 1.33301 9.02021 1.33301 8.66659V2.66659C1.33301 2.31296 1.47348 1.97382 1.72353 1.72378C1.97358 1.47373 2.31272 1.33325 2.66634 1.33325H8.66634C9.01996 1.33325 9.3591 1.47373 9.60915 1.72378C9.8592 1.97382 9.99967 2.31296 9.99967 2.66659V3.33325"
+      stroke="#06AFF2"
+      strokeWidth="2"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+    />
+  </svg>
+);
+
+const CopyButton: FC<CopyButtonProps> = props => {
+  const { label, icon, className, value, ...others } = props;
+  const classes = useStyles();
+  const [tooltipTitle, setTooltipTitle] = useState('Copy');
+
+  const handleClick = (v: string) => {
+    setTooltipTitle('Copied!');
+    navigator.clipboard.writeText(v);
+    setTimeout(() => {
+      setTooltipTitle('Copy');
+    }, 1000);
+  };
+
+  return (
+    <Tooltip
+      title={tooltipTitle}
+      arrow
+      placement="top"
+      TransitionComponent={Fade}
+      TransitionProps={{ timeout: 600 }}
+      className={classes.tooltip}
+    >
+      <IconButton
+        aria-label={label}
+        className={`${classes.button} ${className}`}
+        onClick={() => handleClick(value || '')}
+        {...others}
+      >
+        {icon || CopyIcon}
+      </IconButton>
+    </Tooltip>
+  );
+};
+
+CopyButton.propTypes = {
+  className: PropTypes.string,
+  icon: PropTypes.element,
+  label: PropTypes.string.isRequired,
+  value: PropTypes.string.isRequired,
+  others: PropTypes.object,
+};
+
+CopyButton.defaultProps = {
+  label: 'copy button',
+  value: '',
+};
+
+CopyButton.displayName = 'CopyButton';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    button: {},
+    tooltip: {},
+  })
+);
+
+export default CopyButton;

+ 227 - 0
client/src/components/advancedSearch/Dialog.tsx

@@ -0,0 +1,227 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  Typography,
+  Button,
+  IconButton,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+} from '@material-ui/core';
+import CloseIcon from '@material-ui/icons/Close';
+import CachedIcon from '@material-ui/icons/Cached';
+import ConditionGroup from './ConditionGroup';
+import CopyBtn from './CopyButton';
+
+interface DialogProps {
+  others?: object;
+  open: boolean;
+  onClose: () => void;
+  onSubmit: (data: any) => void;
+  onReset: () => void;
+  onCancel: () => void;
+  title: string;
+  fields: Field[];
+  handleConditions: any;
+  conditions: any[];
+  isLegal: boolean;
+  expression: string;
+}
+
+interface Field {
+  name: string;
+  type: 'int' | 'float';
+}
+
+const AdvancedDialog = React.forwardRef((props: DialogProps, ref) => {
+  const {
+    open,
+    onClose,
+    onSubmit,
+    onReset,
+    onCancel,
+    handleConditions,
+    conditions: flatConditions,
+    isLegal,
+    expression: filterExpression,
+    title,
+    fields,
+    ...others
+  } = props;
+  const { addCondition } = handleConditions;
+  const classes = useStyles();
+
+  useEffect(() => {
+    flatConditions.length === 0 && addCondition();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  // const childRef: any = useRef();
+
+  return (
+    <>
+      <Dialog
+        onClose={onClose}
+        aria-labelledby="customized-dialog-title"
+        open={open}
+        maxWidth="xl"
+        className={classes.wrapper}
+        {...others}
+      >
+        <DialogTitle className={classes.dialogTitle} disableTypography>
+          <Typography variant="h5" component="h2">
+            {title}
+          </Typography>
+          <IconButton
+            aria-label="close"
+            className={classes.closeButton}
+            onClick={onCancel}
+            size="small"
+          >
+            <CloseIcon />
+          </IconButton>
+        </DialogTitle>
+        <DialogContent>
+          <div
+            className={`${classes.expResult} ${
+              !isLegal && 'disable-exp'
+            } testcopy`}
+          >
+            {`${isLegal ? filterExpression : 'Filter Expression'}`}
+            {isLegal && (<CopyBtn label="copy expression" value={filterExpression} />)}
+          </div>
+          <div className={classes.expWrapper}>
+            <ConditionGroup
+              // ref={childRef}
+              fields={fields}
+              handleConditions={handleConditions}
+              conditions={flatConditions}
+            />
+          </div>
+        </DialogContent>
+        <DialogActions className={classes.dialogActions}>
+          <Button
+            onClick={onReset}
+            color="primary"
+            className={classes.resetBtn}
+            size="small"
+          >
+            <CachedIcon />
+            Reset
+          </Button>
+          <div>
+            <Button
+              autoFocus
+              onClick={onCancel}
+              color="primary"
+              className={classes.cancelBtn}
+            >
+              Cancel
+            </Button>
+            <Button
+              onClick={onSubmit}
+              variant="contained"
+              color="primary"
+              className={classes.applyBtn}
+              disabled={!isLegal}
+            >
+              Apply Filters
+            </Button>
+          </div>
+        </DialogActions>
+      </Dialog>
+    </>
+  );
+});
+
+AdvancedDialog.propTypes = {
+  others: PropTypes.object,
+  open: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+  onSubmit: PropTypes.func.isRequired,
+  title: PropTypes.string.isRequired,
+  onReset: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  handleConditions: PropTypes.object.isRequired,
+  conditions: PropTypes.array.isRequired,
+  isLegal: PropTypes.bool.isRequired,
+  expression: PropTypes.string.isRequired,
+};
+
+AdvancedDialog.defaultProps = {
+  open: false,
+  onClose: () => {},
+  onSubmit: () => {},
+  title: 'Advanced Filter',
+  fields: [],
+  onReset: () => {},
+  onCancel: () => {},
+  handleConditions: {},
+  conditions: [],
+  isLegal: false,
+  expression: '',
+};
+
+AdvancedDialog.displayName = 'AdvancedDialog';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      '& .disable-exp': {
+        userSelect: 'none',
+        color: '#AEAEBB',
+      },
+    },
+    closeButton: {
+      color: 'black',
+    },
+    dialogTitle: {
+      display: 'flex',
+      alignItems: 'center',
+      justifyContent: 'space-between',
+    },
+    dialogActions: {
+      justifyContent: 'space-between',
+    },
+    resetBtn: {},
+    cancelBtn: {
+      marginRight: '32px',
+    },
+    applyBtn: {
+      backgroundColor: '#06AFF2',
+      color: 'white',
+    },
+    copyButton: {
+      borderRadius: '0',
+    },
+    expResult: {
+      background: '#F9F9F9',
+      borderRadius: '8px',
+      display: 'flex',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+      minHeight: '40px',
+      marginBottom: '16px',
+      padding: '0 16px',
+      fontFamily: 'Source Code Pro',
+      fontStyle: 'normal',
+      fontWeight: 'normal',
+      fontSize: '16px',
+      lineHeight: '24px',
+    },
+    expWrapper: {
+      background: '#F9F9F9',
+      borderRadius: '8px',
+      minWidth: '480px',
+      minHeight: '104px',
+      padding: '12px',
+    },
+  })
+);
+
+export default AdvancedDialog;

+ 384 - 0
client/src/components/advancedSearch/Filter.tsx

@@ -0,0 +1,384 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  Button,
+  Chip,
+  Tooltip,
+} from '@material-ui/core';
+import FilterListIcon from '@material-ui/icons/FilterList';
+
+import AdvancedDialog from './Dialog';
+
+interface FilterProps {
+  className?: string;
+  title: string;
+  showTitle?: boolean;
+  others?: object;
+  onSubmit: (data: any) => void;
+  tooltipPlacement?: 'left' | 'right' | 'bottom' | 'top';
+  fields: Field[];
+}
+
+interface Field {
+  name: string;
+  type: 'int' | 'float';
+}
+
+const generateHashCode = (source: string) => {
+  var hash = 0,
+    i,
+    chr;
+  if (source.length === 0) return hash;
+  for (i = 0; i < source.length; i++) {
+    chr = source.charCodeAt(i);
+    hash = (hash << 5) - hash + chr;
+    hash |= 0; // Convert to 32bit integer
+  }
+  return hash.toString();
+};
+
+const Filter = React.forwardRef(function Filter(props: FilterProps, ref) {
+  const {
+    title,
+    showTitle,
+    className,
+    tooltipPlacement,
+    onSubmit,
+    fields,
+    ...others
+  } = props;
+  const classes = useStyles();
+
+  const [open, setOpen] = useState(false);
+  const [conditionSum, setConditionSum] = useState(0);
+  const [flatConditions, setFlatConditions] = useState<any[]>([]);
+  const [initConditions, setInitConditions] = useState<any[]>([]);
+  const [isConditionsLegal, setIsConditionsLegal] = useState(false);
+  const [filterExpression, setFilterExpression] = useState('');
+
+  useEffect(() => {
+    setInitConditions(flatConditions);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  // Check all conditions are all correct.
+  useEffect(() => {
+    // Calc the sum of conditions.
+    setConditionSum(flatConditions.filter(i => i.type === 'condition').length);
+    if (flatConditions.length === 0) {
+      setIsConditionsLegal(false);
+      return;
+    }
+    for (let i = 0; i < flatConditions.length; i++) {
+      const { data, type } = flatConditions[i];
+      if (type !== 'condition') continue;
+      if (!data) {
+        setIsConditionsLegal(false);
+        return;
+      }
+      if (!data.isCorrect) {
+        setIsConditionsLegal(data.isCorrect);
+        return;
+      }
+    }
+    setIsConditionsLegal(true);
+    generateExpression(flatConditions, setFilterExpression);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [flatConditions]);
+
+  const setFilteredFlatConditions = (conditions: any[]) => {
+    const newConditions = conditions.reduce((prev, item, currentIndex) => {
+      if (prev.length === 0 && item.type !== 'condition') return prev;
+      if (
+        prev.length &&
+        item.type !== 'condition' &&
+        prev[prev.length - 1].type !== 'condition'
+      )
+        return prev;
+      return [...prev, item];
+    }, []);
+    setFlatConditions(newConditions);
+  };
+
+  const generateExpression = (conditions: any[], func: any) => {
+    const expression = conditions.reduce((prev, item) => {
+      const { type, data } = item;
+      if (type === 'break') return `${prev} || `;
+      const {
+        field: { name },
+        op,
+        value,
+      } = data;
+      return `${prev}${
+        prev && !prev.endsWith('|| ') ? ' && ' : ''
+      }${name} ${op} ${value}`;
+    }, '');
+    func(expression);
+  };
+
+  // Generate id for each element.
+  const generateId = (salt?: string) => {
+    return generateHashCode(`${new Date().getTime().toString()}-${salt}`);
+  };
+
+  /**
+   * Insert "OR" operator into specified position.
+   * @param targetId The break operator will be inserted after the target one.
+   */
+  const addBreak = (targetId?: string) => {
+    if (!targetId) {
+      setFilteredFlatConditions([
+        ...flatConditions,
+        { id: generateId('break'), type: 'break' },
+        {
+          id: generateId('condition'),
+          type: 'condition',
+          field: '',
+          operator: '<',
+          value: '',
+        },
+      ]);
+      return;
+    }
+    const formerConditons = [...flatConditions];
+    const newConditions = formerConditons.reduce((prev, item) => {
+      if (item.id === targetId) {
+        return [...prev, item, { id: generateId('break'), type: 'break' }];
+      }
+      return [...prev, item];
+    }, []);
+    setFilteredFlatConditions(newConditions);
+  };
+  /**
+   * Remove "OR" operator in specified position.
+   * @param targetId Remove the break operator after the target one.
+   */
+  const removeBreak = (targetId: string) => {
+    setFilteredFlatConditions(flatConditions.filter(i => i.id !== targetId));
+  };
+  /**
+   * Add new condition to specified position.
+   * @param targetId Position where new condition item will be inserted.
+   * Will be pushed to tail if empty.
+   * @param beforeTarget Will be inserted before the target item.
+   */
+  const addCondition = (targetId?: string, beforeTarget?: boolean) => {
+    const formerConditons = [...flatConditions];
+    const newItem = {
+      id: generateId('condition'),
+      type: 'condition',
+      field: '',
+      operator: '<',
+      value: '',
+    };
+    if (!targetId) {
+      formerConditons.push(newItem);
+      setFilteredFlatConditions(formerConditons);
+      return;
+    }
+    const newConditions = formerConditons.reduce((prev, item) => {
+      if (item.id === targetId) {
+        const newItems = [
+          item,
+          {
+            id: generateId('condition'),
+            type: 'condition',
+            field: '',
+            operator: '<',
+            value: '',
+          },
+        ];
+        beforeTarget && newItems.reverse();
+        return [...prev, ...newItems];
+      }
+      return [...prev, item];
+    }, []);
+    setIsConditionsLegal(false);
+    setFilteredFlatConditions(newConditions);
+  };
+  /**
+   * Remove condition from specified position.
+   * @param targetId Position where new condition item will be removed.
+   */
+  const removeCondition = (targetId: string) => {
+    setFilteredFlatConditions(flatConditions.filter(i => i.id !== targetId));
+    // flatConditions.reduce((prev, item, currentIndex) => {
+    //   if (item.id === targetId) {
+
+    //   }
+    // }, []);
+  };
+  const changeBinaryLogicalOp = (value: string, targetId: string) => {
+    if (value === 'or') {
+      addBreak(targetId);
+    } else if (value === 'and') {
+      removeBreak(targetId);
+    }
+  };
+  /**
+   * Update specified condition's data.
+   * @param id Specify one item that will be updated.
+   * @param data Data that will be updated to condition.
+   */
+  const updateConditionData = (
+    id: string,
+    data: { field: Field; op: string; value: string; isCorrect: boolean }
+  ) => {
+    const formerFlatConditions = flatConditions.map(i => {
+      if (i.id === id) return { ...i, data };
+      return i;
+    });
+    setFilteredFlatConditions(formerFlatConditions);
+  };
+  // Handle condition change.
+  const onConditionChange = (data: any) => {
+    const { field, op, value, isCorrect, id } = data;
+    updateConditionData(id, { field, op, value, isCorrect });
+  };
+  // Reset conditions.
+  const resetConditions = () => {
+    setIsConditionsLegal(false);
+    setFilteredFlatConditions([
+      {
+        id: generateId(),
+        type: 'condition',
+        field: '',
+        operator: '<',
+        value: '',
+      },
+    ]);
+  };
+  const getConditionsSum = () => {
+    const conds = flatConditions.filter(i => i.type === 'condition');
+    return conds.length;
+  };
+
+  const handleConditions = {
+    addBreak,
+    removeBreak,
+    addCondition,
+    removeCondition,
+    changeBinaryLogicalOp,
+    updateConditionData,
+    onConditionChange,
+    resetConditions,
+    getConditionsSum,
+    setFilteredFlatConditions,
+    setIsConditionsLegal,
+    setFilterExpression,
+  };
+
+  const handleClickOpen = () => {
+    setOpen(true);
+  };
+  const handleClose = () => {
+    setOpen(false);
+  };
+  const handleDeleteAll = () => {
+    setFlatConditions([]);
+    setInitConditions([]);
+    onSubmit('');
+  };
+  const handleCancel = () => {
+    setOpen(false);
+    handleFallback();
+  };
+  const handleSubmit = () => {
+    onSubmit(filterExpression);
+    setInitConditions(flatConditions);
+    setOpen(false);
+  };
+  const handleReset = () => {
+    setFilteredFlatConditions([
+      {
+        id: generateId('condition'),
+        type: 'condition',
+        field: '',
+        operator: '<',
+        value: '',
+      },
+    ]);
+  };
+  const handleFallback = () => {
+    setFilteredFlatConditions(initConditions);
+  };
+
+  return (
+    <>
+      <div className={`${classes.wrapper} ${className}`} {...others}>
+        <Button className={`${classes.afBtn} af-btn`} onClick={handleClickOpen}>
+          <FilterListIcon />
+          {showTitle ? title : ''}
+        </Button>
+        {conditionSum > 0 && (
+          <Tooltip
+            arrow
+            interactive
+            title={filterExpression}
+            placement={tooltipPlacement}
+          >
+            <Chip
+              label={conditionSum}
+              onDelete={handleDeleteAll}
+              variant="outlined"
+              size="small"
+            />
+          </Tooltip>
+        )}
+        {open && (
+          <AdvancedDialog
+            open={open}
+            onClose={handleClose}
+            onCancel={handleCancel}
+            onSubmit={handleSubmit}
+            onReset={handleReset}
+            title="Advanced Filter"
+            fields={fields}
+            handleConditions={handleConditions}
+            conditions={flatConditions}
+            isLegal={isConditionsLegal}
+            expression={filterExpression}
+          />
+        )}
+      </div>
+    </>
+  );
+});
+
+Filter.propTypes = {
+  className: PropTypes.string,
+  /**
+   * Specify the title content
+   */
+  title: PropTypes.string.isRequired,
+  /**
+   * Specify whether the title should be visiable
+   */
+  showTitle: PropTypes.bool,
+  onSubmit: PropTypes.func.isRequired,
+  tooltipPlacement: PropTypes.oneOf(['left', 'right', 'bottom', 'top']),
+};
+
+Filter.defaultProps = {
+  className: '',
+  showTitle: true,
+  tooltipPlacement: 'top',
+  fields: [],
+  onSubmit: () => {},
+};
+
+Filter.displayName = 'AdvancedFilter';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    wrapper: {},
+    afBtn: {
+      color: '#06AFF2',
+    },
+  })
+);
+
+export default Filter;

+ 3 - 0
client/src/components/advancedSearch/index.tsx

@@ -0,0 +1,3 @@
+import Filter from './Filter';
+
+export default Filter;