@ant-design/icons#SearchOutlined TypeScript Examples

The following examples show how to use @ant-design/icons#SearchOutlined. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: general-search.editor.tsx    From next-basics with GNU General Public License v3.0 6 votes vote down vote up
export function GeneralSearchEditor({
  nodeUid,
}: EditorComponentProps): React.ReactElement {
  const node = useBuilderNode<GeneralSearchProperties>({ nodeUid });
  const { placeholder } = node.$$parsedProperties;

  return (
    <EditorContainer nodeUid={nodeUid}>
      <div className={styles.searchContainer}>
        <div className={styles.placeholder}>
          {smartDisplayForEvaluableString(placeholder, "", "<% … %>")}
        </div>
        <SearchOutlined className={styles.searchIcon} />
      </div>
    </EditorContainer>
  );
}
Example #2
Source File: Searchbar.tsx    From datart with Apache License 2.0 6 votes vote down vote up
export function Searchbar({ placeholder, onSearch }: SearchbarProps) {
  return (
    <Wrapper className="search">
      <Col span={24}>
        <Input
          prefix={<SearchOutlined className="icon" />}
          placeholder={placeholder}
          className="search-input"
          bordered={false}
          onChange={onSearch}
        />
      </Col>
    </Wrapper>
  );
}
Example #3
Source File: metricsExplorer.tsx    From fe-v5 with Apache License 2.0 6 votes vote down vote up
MetricsExplorer: React.FC<MetricsExplorer> = ({ show, updateShow, metrics, insertAtCursor }) => {
  const [filteredMetrics, setFilteredMetrics] = useState<string[]>(metrics);

  function checkMetric(value: string) {
    insertAtCursor(value);
    updateShow(false);
  }

  useEffect(() => {
    setFilteredMetrics(metrics);
  }, [metrics]);

  return (
    <Modal className='metrics-explorer-modal' width={540} visible={show} title='Metrics Explorer' footer={null} onCancel={() => updateShow(false)}>
      <Input
        className='left-area-group-search'
        prefix={<SearchOutlined />}
        onPressEnter={(e) => {
          e.preventDefault();
          const value = e.currentTarget.value;
          setFilteredMetrics(metrics.filter((metric) => metric.includes(value)));
        }}
      />
      <div className='metric-list' onClick={(e) => checkMetric((e.target as HTMLElement).innerText)}>
        {filteredMetrics.map((metric) => (
          <div className='metric-list-item' key={metric}>
            {metric}
          </div>
        ))}
      </div>
    </Modal>
  );
}
Example #4
Source File: index.tsx    From datart with Apache License 2.0 6 votes vote down vote up
export function NotFoundPage() {
  const t = useI18NPrefix('notfound');
  return (
    <Wrapper>
      <Helmet>
        <title>{`404 ${t('title')}`}</title>
        <meta name="description" content={t('title')} />
      </Helmet>
      <h1>
        <SearchOutlined className="icon" />
        404
      </h1>
      <h2>{t('title')}</h2>
    </Wrapper>
  );
}
Example #5
Source File: Table.stories.tsx    From ant-table-extensions with MIT License 6 votes vote down vote up
CustomSeachInput = () => {
  return (
    <Table
      dataSource={dataSource}
      columns={columns}
      searchableProps={{
        // dataSource,
        // setDataSource: setSearchDataSource,
        inputProps: {
          placeholder: "Search this table...",
          prefix: <SearchOutlined />,
        },
      }}
    />
  );
}
Example #6
Source File: index.tsx    From nebula-studio with Apache License 2.0 6 votes vote down vote up
Search = (props: IProps) => {
  const { onSearch, type } = props;
  const location = useLocation();
  const [value, setValue] = useState('');
  useEffect(() => {
    setValue('');
  }, [location.pathname]);
  return (
    <div className={styles.schemaSearch}>
      <Input
        value={value}
        prefix={<SearchOutlined className={styles.inputSearch} onClick={() => onSearch(value)} />}
        allowClear={true}
        placeholder={intl.get('common.namePlaceholder', { name: type })}
        onChange={e => setValue(e.target.value)}
        onPressEnter={() => onSearch(value)}
        style={{ width: 300 }}
        suffix={null}
      />
    </div>
  );
}
Example #7
Source File: index.tsx    From fe-v5 with Apache License 2.0 6 votes vote down vote up
BaseSearchInput: React.FC<IBaseSearchInputProps> = ({
  onSearch,
  ...props
}) => {
  const { t } = useTranslation();
  const [value, setValue] = useState<string>('');
  return (
    <Input
      prefix={<SearchOutlined />}
      value={value}
      onChange={(e) => {
        setValue(e.target.value);

        if (e.target.value === '') {
          onSearch && onSearch('');
        }
      }}
      onPressEnter={(e) => {
        onSearch && onSearch(value);
      }}
      {...props}
    />
  );
}
Example #8
Source File: searchBox.tsx    From metaplex with Apache License 2.0 6 votes vote down vote up
SearchBox = () => {
  return (
    <Button
      className="search-btn"
      shape="circle"
      icon={<SearchOutlined />}
    ></Button>
  );
}
Example #9
Source File: index.tsx    From visual-layout with MIT License 6 votes vote down vote up
Components: React.FC<{}> = () => {
  const [value, setValue] = useState('');
  const { appService } = useContext(AppContext);
  const pageService = appService.project.getCurrentPage();

  return useMemo(
    () => (
      <div className={styles.container}>
        <div className={styles.search}>
          <components.Input
            placeholder="组件搜索"
            onChange={e => setValue(e.target.value)}
            addonAfter={<SearchOutlined />}
          />
        </div>
        <div className={styles.components}>
          {ComponentsAST.filter(component => {
            return (
              !value ||
              !component.children ||
              (typeof component.children !== 'string' &&
                component.children.every(child => {
                  return new RegExp(`${_.escapeRegExp(value)}`, 'ig').test(
                    isString(child) ? child : child._name,
                  );
                }))
            );
          }).map((ast, index) => (
            <Component key={index} ast={ast} pageService={pageService} />
          ))}
        </div>
      </div>
    ),
    [pageService, value],
  );
}
Example #10
Source File: Participants.tsx    From nanolooker with MIT License 6 votes vote down vote up
Progress: React.FC<ProgressProps> = ({ isCompleted, hash }) => {
  const { theme } = React.useContext(PreferencesContext);

  return isCompleted || (hash && hash !== "0") ? (
    <Space size={6}>
      <CheckCircleTwoTone
        twoToneColor={
          theme === Theme.DARK ? TwoToneColors.RECEIVE_DARK : "#52c41a"
        }
      />
      {hash && hash !== "0" ? (
        <Link to={`/block/${hash}`}>
          <Button shape="circle" size="small" icon={<SearchOutlined />} />
        </Link>
      ) : null}
    </Space>
  ) : (
    <CloseCircleOutlined />
  );
}
Example #11
Source File: general-menu.editor.tsx    From next-basics with GNU General Public License v3.0 6 votes vote down vote up
export function GeneralMenuEditor({
  nodeUid,
  editorProps,
}: EditorComponentProps): React.ReactElement {
  const node = useBuilderNode<GeneralMenuProperties>({ nodeUid });

  return (
    <EditorContainer nodeUid={nodeUid}>
      <div className={styles.menuContainer}>
        {editorProps?.showSearch && (
          <div className={styles.search}>
            <SearchOutlined className={styles.searchIcon} />
          </div>
        )}
        <div className={styles.title}></div>
        {range(0, 3).map((_, index) => (
          <div key={index} className={styles.item}>
            <div className={styles.icon}></div>
            <div className={styles.text}></div>
          </div>
        ))}
      </div>
    </EditorContainer>
  );
}
Example #12
Source File: SearchBox.tsx    From posthog-foss with MIT License 6 votes vote down vote up
export function SearchBox(): JSX.Element {
    const { showPalette } = useActions(commandPaletteLogic)

    return (
        <div className="SearchBox" onClick={showPalette} data-attr="command-palette-toggle">
            <div className="SearchBox__primary-area">
                <SearchOutlined className="SearchBox__magnifier" />
                Search
            </div>
            <div className="SearchBox__keyboard-shortcut">{platformCommandControlKey('K')}</div>
        </div>
    )
}
Example #13
Source File: WorkbenchTree.tsx    From next-basics with GNU General Public License v3.0 5 votes vote down vote up
export function WorkbenchTree({
  nodes,
  placeholder,
  searchPlaceholder,
  noSearch,
}: WorkbenchTreeProps): ReactElement {
  const [q, setQ] = useState<string>(null);

  const handleSearchChange = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      setQ(event.target.value);
    },
    []
  );

  const { matchNode } = useWorkbenchTreeContext();

  const trimmedLowerQ = q?.trim().toLowerCase();
  const filteredNodes = useMemo(() => {
    if (noSearch || !trimmedLowerQ || !nodes) {
      return nodes;
    }
    const walk = (node: WorkbenchNodeData): boolean => {
      node.matchedSelf = matchNode(node, trimmedLowerQ);
      const hasMatchedChildren = node.children?.map(walk).includes(true);
      node.matched = node.matchedSelf || hasMatchedChildren;
      return node.matched;
    };
    nodes.forEach(walk);
    return nodes.slice();
  }, [noSearch, trimmedLowerQ, nodes, matchNode]);

  return nodes?.length ? (
    <div>
      {!noSearch && (
        <div className={styles.searchBox}>
          <Input
            value={q}
            onChange={handleSearchChange}
            size="small"
            placeholder={searchPlaceholder}
            prefix={<SearchOutlined />}
            allowClear
          />
        </div>
      )}
      <SearchingContext.Provider value={!!q}>
        <TreeList nodes={filteredNodes} level={1} />
      </SearchingContext.Provider>
    </div>
  ) : (
    <div className={styles.placeholder}>{placeholder}</div>
  );
}
Example #14
Source File: PropertyNamesSelect.tsx    From posthog-foss with MIT License 5 votes vote down vote up
PropertyNamesSearch = (): JSX.Element => {
    const { query, filteredProperties, isSelected } = useValues(propertySelectLogic)
    const { setQuery, toggleProperty } = useActions(propertySelectLogic)

    return (
        <>
            <Input
                onChange={({ target: { value } }) => setQuery(value)}
                allowClear
                className="search-box"
                placeholder="Search for properties"
                prefix={<SearchOutlined />}
            />
            <div className="search-results">
                {filteredProperties.length ? (
                    filteredProperties.map((property) => (
                        <Checkbox
                            key={property.name}
                            className={'checkbox' + (isSelected(property.name) ? ' checked' : '')}
                            checked={isSelected(property.name)}
                            onChange={() => toggleProperty(property.name)}
                        >
                            {property.highlightedNameParts.map((part, index) =>
                                part.toLowerCase() === query.toLowerCase() ? (
                                    <b key={index}>{part}</b>
                                ) : (
                                    <React.Fragment key={index}>{part}</React.Fragment>
                                )
                            )}
                        </Checkbox>
                    ))
                ) : (
                    <p className="no-results-message">
                        No properties match <b>“{query}”</b>. Refine your search to try again.
                    </p>
                )}
            </div>
        </>
    )
}
Example #15
Source File: index.tsx    From ant-design-pro-V5-multitab with MIT License 5 votes vote down vote up
HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
  const {
    className,
    defaultValue,
    onVisibleChange,
    placeholder,
    visible,
    defaultVisible,
    ...restProps
  } = props;

  const inputRef = useRef<Input | null>(null);

  const [value, setValue] = useMergeValue<string | undefined>(defaultValue, {
    value: props.value,
    onChange: props.onChange,
  });

  const [searchMode, setSearchMode] = useMergeValue(defaultVisible ?? false, {
    value: props.visible,
    onChange: onVisibleChange,
  });

  const inputClass = classNames(styles.input, {
    [styles.show]: searchMode,
  });
  return (
    <div
      className={classNames(className, styles.headerSearch)}
      onClick={() => {
        setSearchMode(true);
        if (searchMode && inputRef.current) {
          inputRef.current.focus();
        }
      }}
      onTransitionEnd={({ propertyName }) => {
        if (propertyName === 'width' && !searchMode) {
          if (onVisibleChange) {
            onVisibleChange(searchMode);
          }
        }
      }}
    >
      <SearchOutlined
        key="Icon"
        style={{
          cursor: 'pointer',
        }}
      />
      <AutoComplete
        key="AutoComplete"
        className={inputClass}
        value={value}
        options={restProps.options}
        onChange={setValue}
      >
        <Input
          size="small"
          ref={inputRef}
          defaultValue={defaultValue}
          aria-label={placeholder}
          placeholder={placeholder}
          onKeyDown={(e) => {
            if (e.key === 'Enter') {
              if (restProps.onSearch) {
                restProps.onSearch(value);
              }
            }
          }}
          onBlur={() => {
            setSearchMode(false);
          }}
        />
      </AutoComplete>
    </div>
  );
}
Example #16
Source File: index.tsx    From anew-server with MIT License 5 votes vote down vote up
HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
  const {
    className,
    defaultValue,
    onVisibleChange,
    placeholder,
    visible,
    defaultVisible,
    ...restProps
  } = props;

  const inputRef = useRef<Input | null>(null);

  const [value, setValue] = useMergedState<string | undefined>(defaultValue, {
    value: props.value,
    onChange: props.onChange,
  });

  const [searchMode, setSearchMode] = useMergedState(defaultVisible ?? false, {
    value: props.visible,
    onChange: onVisibleChange,
  });

  const inputClass = classNames(styles.input, {
    [styles.show]: searchMode,
  });
  return (
    <div
      className={classNames(className, styles.headerSearch)}
      onClick={() => {
        setSearchMode(true);
        if (searchMode && inputRef.current) {
          inputRef.current.focus();
        }
      }}
      onTransitionEnd={({ propertyName }) => {
        if (propertyName === 'width' && !searchMode) {
          if (onVisibleChange) {
            onVisibleChange(searchMode);
          }
        }
      }}
    >
      <SearchOutlined
        key="Icon"
        style={{
          cursor: 'pointer',
        }}
      />
      <AutoComplete
        key="AutoComplete"
        className={inputClass}
        value={value}
        options={restProps.options}
        onChange={setValue}
      >
        <Input
          size="small"
          ref={inputRef}
          defaultValue={defaultValue}
          aria-label={placeholder}
          placeholder={placeholder}
          onKeyDown={(e) => {
            if (e.key === 'Enter') {
              if (restProps.onSearch) {
                restProps.onSearch(value);
              }
            }
          }}
          onBlur={() => {
            setSearchMode(false);
          }}
        />
      </AutoComplete>
    </div>
  );
}
Example #17
Source File: Worklist.tsx    From slim with Apache License 2.0 5 votes vote down vote up
getColumnSearchProps = (dataIndex: string): object => ({
    filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }: {
      setSelectedKeys: (selectedKeys: React.Key[]) => void
      selectedKeys: React.Key[]
      confirm: (params?: FilterConfirmProps) => void
      clearFilters: () => void
    }) => (
      <div style={{ padding: 8 }}>
        <Input
          placeholder='Search'
          value={selectedKeys[0]}
          onChange={e => setSelectedKeys(
            e.target.value !== undefined ? [e.target.value] : []
          )}
          onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
          style={{ width: 188, marginBottom: 8, display: 'block' }}
        />
        <Space>
          <Button
            type='primary'
            onClick={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
            icon={<SearchOutlined />}
            size='small'
            style={{ width: 90 }}
          >
            Search
          </Button>
          <Button
            onClick={() => this.handleReset(clearFilters)}
            size='small'
            style={{ width: 90 }}
          >
            Reset
          </Button>
        </Space>
      </div>
    ),
    filterIcon: (filtered: boolean) => (
      <SearchOutlined
        style={{ color: filtered ? '#1890ff' : undefined }}
      />
    )
  })
Example #18
Source File: SearchBar.tsx    From next-basics with GNU General Public License v3.0 5 votes vote down vote up
export function SearchBar(props: SearchBarProps): React.ReactElement {
  const [focus, setFocus] = React.useState(false);

  const searchInputRef = React.useCallback((element) => {
    if (element) {
      // Wait for portal mounted first.
      Promise.resolve().then(() => {
        try {
          element.focus();
        } catch (e) {
          // Do nothing.
        }
      });
    }
  }, []);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    props.onChange(e.target.value);
  };

  const handleClick = (e: React.MouseEvent): void => {
    e.stopPropagation();
  };

  const handleKeyDown = (e: React.KeyboardEvent): void => {
    const key =
      e.key ||
      /* istanbul ignore next: compatibility */ e.keyCode ||
      /* istanbul ignore next: compatibility */ e.which;
    if (
      [
        "Tab",
        "Enter",
        "ArrowLeft",
        "ArrowUp",
        "ArrowRight",
        "ArrowDown",
        9,
        13,
        37,
        38,
        39,
        40,
      ].includes(key)
    ) {
      e.preventDefault();
    }
  };

  const handleFocus = (): void => {
    setFocus(true);
  };

  const handleBlur = (): void => {
    setFocus(false);
  };

  return (
    <div
      className={classNames(styles.searchBar, {
        [styles.focus]: focus,
      })}
    >
      <div className={styles.inputContainer} onClick={handleClick}>
        <Input
          placeholder={i18next.t(
            `${NS_BASIC_BRICKS}:${K.SEARCH_BY_NAME_KEYWORD}`
          )}
          onChange={handleChange}
          onKeyDown={handleKeyDown}
          onFocus={handleFocus}
          onBlur={handleBlur}
          prefix={<SearchOutlined />}
          ref={searchInputRef}
        />
      </div>
    </div>
  );
}
Example #19
Source File: index.tsx    From fe-v5 with Apache License 2.0 5 votes vote down vote up
index = (_props: any) => {
  const history = useHistory();
  const { t, i18n } = useTranslation();
  const [query, setQuery] = useState('');
  const [mine, setMine] = useState(true);
  const [days, setDays] = useState(7);
  const { curBusiItem } = useSelector<RootState, CommonStoreState>((state) => state.common);
  const { tableProps } = useAntdTable((options) => getTableData(options, curBusiItem.id, query, mine, days), { refreshDeps: [curBusiItem.id, query, mine, days] });
  const columns: ColumnProps<DataItem>[] = [
    {
      title: 'ID',
      dataIndex: 'id',
      width: 100,
    },
    {
      title: t('task.title'),
      dataIndex: 'title',
      width: 200,
      render: (text, record) => {
        return <Link to={{ pathname: `/job-tasks/${record.id}/result` }}>{text}</Link>;
      },
    },
    {
      title: t('table.operations'),
      width: 150,
      render: (_text, record) => {
        return (
          <span>
            <Link to={{ pathname: '/job-tasks/add', search: `task=${record.id}` }}>{t('task.clone')}</Link>
            <Divider type='vertical' />
            <Link to={{ pathname: `/job-tasks/${record.id}/detail` }}>{t('task.meta')}</Link>
          </span>
        );
      },
    },
    {
      title: t('task.creator'),
      dataIndex: 'create_by',
      width: 100,
    },
    {
      title: t('task.created'),
      dataIndex: 'create_at',
      width: 160,
      render: (text) => {
        return moment.unix(text).format('YYYY-MM-DD HH:mm:ss');
      },
    },
  ];
  return (
    <PageLayout
      hideCluster
      title={
        <>
          <CodeOutlined />
          {t('执行历史')}
        </>
      }
    >
      <div style={{ display: 'flex' }}>
        <LeftTree></LeftTree>
        {curBusiItem.id ? (
          <div style={{ flex: 1, padding: 10 }}>
            <Row>
              <Col span={16} className='mb10'>
                <Input
                  style={{ width: 200, marginRight: 10 }}
                  prefix={<SearchOutlined />}
                  defaultValue={query}
                  onPressEnter={(e) => {
                    setQuery(e.currentTarget.value);
                  }}
                  placeholder='搜索标题'
                />
                <Select
                  style={{ marginRight: 10 }}
                  value={days}
                  onChange={(val: number) => {
                    setDays(val);
                  }}
                >
                  <Select.Option value={7}>{t('last.7.days')}</Select.Option>
                  <Select.Option value={15}>{t('last.15.days')}</Select.Option>
                  <Select.Option value={30}>{t('last.30.days')}</Select.Option>
                  <Select.Option value={60}>{t('last.60.days')}</Select.Option>
                  <Select.Option value={90}>{t('last.90.days')}</Select.Option>
                </Select>
                <Checkbox
                  checked={mine}
                  onChange={(e) => {
                    setMine(e.target.checked);
                  }}
                >
                  {t('task.only.mine')}
                </Checkbox>
              </Col>
              <Col span={8} style={{ textAlign: 'right' }}>
                <Button
                  type='primary'
                  ghost
                  onClick={() => {
                    history.push('/job-tasks/add');
                  }}
                >
                  {t('task.temporary.create')}
                </Button>
              </Col>
            </Row>
            <Table
              rowKey='id'
              columns={columns as any}
              {...(tableProps as any)}
              pagination={
                {
                  ...tableProps.pagination,
                  showSizeChanger: true,
                  pageSizeOptions: ['10', '50', '100', '500', '1000'],
                  showTotal: (total) => {
                    return i18n.language == 'en' ? `Total ${total} items` : `共 ${total} 条`;
                  },
                } as any
              }
            />
          </div>
        ) : (
          <BlankBusinessPlaceholder text={t('执行历史')}></BlankBusinessPlaceholder>
        )}
      </div>
    </PageLayout>
  );
}
Example #20
Source File: Modal.tsx    From nanolooker with MIT License 5 votes vote down vote up
QRCodeModal = ({ header, account, children }: QRCodeModalProps) => {
  const { t } = useTranslation();
  const [isVisible, setIsVisible] = React.useState(false);
  const { account: accountParam = "" } = useParams<PageParams>();

  return (
    <>
      {React.cloneElement(children, {
        onClick: () => {
          setIsVisible(true);
        },
      })}
      <Modal
        width="300px"
        visible={isVisible}
        onCancel={() => setIsVisible(false)}
        footer={[
          <Button
            key="submit"
            type="primary"
            onClick={() => setIsVisible(false)}
          >
            {t("common.ok")}
          </Button>,
        ]}
      >
        {header}
        <div style={{ textAlign: "center" }}>
          <QRCode account={account} />
        </div>
        <>
          {(account === DONATION_ACCOUNT &&
            accountParam !== DONATION_ACCOUNT) ||
          [
            NANOBROWSERQUEST_DONATION_ACCOUNT,
            NANOQUAKEJS_DONATION_ACCOUNT,
          ].includes(account) ? (
            <div
              style={{
                display: "flex",
                justifyContent: "center",
                marginBottom: "12px",
              }}
            >
              <Copy text={account} />
              <Link to={`/account/${account}`} style={{ marginLeft: "12px" }}>
                <Button
                  shape="circle"
                  size="small"
                  icon={<SearchOutlined />}
                  onClick={() => setIsVisible(false)}
                />
              </Link>
            </div>
          ) : null}
          <Text>{account}</Text>
        </>
      </Modal>
    </>
  );
}
Example #21
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
StaffTreeSelectionModal: React.FC<StaffSelectionProps> = (props) => {
  const { visible, setVisible, defaultCheckedStaffs, onFinish, allStaffs } = props;
  const [staffs, setStaffs] = useState<StaffOption[]>([]);
  const [staffNodes, setStaffNodes] = useState<TreeNode[]>([]); // 一维的节点
  const [staffTree, setStaffTree] = useState<TreeNode[]>([]); // 多维的树节点
  const [selectedStaffs, setSelectedStaffs] = useState<StaffOption[]>(defaultCheckedStaffs || []);
  const [keyword, setKeyword] = useState<string>('');
  const [checkAll, setCheckAll] = useState<boolean>(false);
  const [expandAll, setExpandAll] = useState<boolean>(false);
  const [checkedNodeKeys, setCheckedNodeKeys] = useState<string[]>([]);
  const [expandedNodeKeys, setExpandedNodeKeys] = useState<string[]>([rootNode]);
  const allStaffMap = _.keyBy(allStaffs, 'ext_id');

  const onCheckAllChange = (e: CheckboxChangeEvent) => {
    let items: StaffOption[];
    if (e.target.checked) {
      items = _.uniqWith<StaffOption>(
        [...staffs, ...selectedStaffs],
        (a, b) => a.ext_id === b.ext_id,
      );
    } else {
      // @ts-ignore
      items = _.differenceWith<StaffParam>(selectedStaffs, staffs, (a, b) => a.ext_id === b.ext_id);
    }
    setSelectedStaffs(items);
    setCheckAll(e.target.checked);
  };

  const onNodesCheck = (checked: string[]) => {
    const checkedExtStaffIDs: string[] = [];
    let selectedExtStaffIDs = _.map(selectedStaffs, 'ext_id');
    let checkedKeys = [...checked];

    // 找出本次uncheck的key,根据这些key的ext_id去删除相关checkedKey
    const uncheckedKeys = _.difference(checkedNodeKeys, checkedKeys);
    _.forEach<string>(uncheckedKeys, (key: string) => {
      const [, , nodeID] = key.split(separator);
      // eslint-disable-next-line no-param-reassign
      // @ts-ignore
      checkedKeys = checkedKeys.filter<string>((checkedKey) => {
        return !checkedKey.includes(`${separator}${nodeID}`);
      });
    });

    // 记录当前所有checked的key
    checkedKeys.forEach((key) => {
      const [nodeType, , nodeID] = key.split(separator);
      if (nodeType === 'node') {
        checkedExtStaffIDs.push(nodeID);
        selectedExtStaffIDs.push(nodeID);
      }
    });

    // 计算需要删除的extStaffID
    // @ts-ignore
    const shouldDeleteExtStaffIDs = _.difference(_.map(staffs, 'ext_id'), checkedExtStaffIDs);
    selectedExtStaffIDs = _.difference(
      _.uniq(selectedExtStaffIDs),
      _.uniq(shouldDeleteExtStaffIDs),
    );

    const items = selectedExtStaffIDs.map((selectedExtStaffID) => {
      return allStaffMap[selectedExtStaffID];
    });

    setCheckAll(staffs.length === items.length);
    setSelectedStaffs(items);
  };

  const nodeRender = (node: DataNode): ReactNode => {
    const [nodeType, , extStaffID] = node.key.toString().split(separator);
    if (nodeType === 'node') {
      const staff = allStaffMap[extStaffID];
      if (staff) {
        return (
          <div className={styles.staffTitleNode}>
            <img src={FormatWeWorkAvatar(staff?.avatar_url, 60)} className={styles.avatar} />
            <div className={styles.text}>
              <span className={styles.title}>{staff?.name}</span>
              <em
                style={{
                  color: RoleColorMap[staff?.role_type],
                  borderColor: RoleColorMap[staff?.role_type],
                }}
              >
                {RoleMap[staff?.role_type] || '普通员工'}
              </em>
            </div>
          </div>
        );
      }
    }
    return (
      <>
        <FolderFilled
          style={{
            color: '#47a7ff',
            fontSize: 20,
            marginRight: 6,
            verticalAlign: -3,
          }}
        />
        {node.title}
      </>
    );
  };

  useEffect(() => {
    setSelectedStaffs(defaultCheckedStaffs || []);
    setKeyword('');
  }, [defaultCheckedStaffs, visible]);

  // 监听选中员工变化,计算checked的树节点
  useEffect(() => {
    const allStaffNodeKeys = _.map(staffNodes, 'key');
    // 计算当前选中的员工,命中的key
    const matchedKeys: string[] = [];
    allStaffNodeKeys.forEach((key: string) => {
      selectedStaffs.forEach((staff) => {
        if (key.includes(`${separator}${staff?.ext_id}`)) {
          matchedKeys.push(key);
        }
      });
    });
    setCheckedNodeKeys(matchedKeys);
  }, [selectedStaffs, staffNodes]);

  // 关键词变化的时候
  useEffect(() => {
    const filteredStaffs = allStaffs.filter((item) => {
      // 搜索部门名称
      let isDepartmentMatch = false;
      item?.departments?.forEach((department) => {
        if (department.name.includes(keyword)) {
          isDepartmentMatch = true;
        }
      });
      return keyword === '' || isDepartmentMatch || item.label.includes(keyword);
    });
    setStaffs(filteredStaffs);
    const { nodes, tree } = buildStaffTree(filteredStaffs);

    // 这里同步更新node节点和选中key值
    let checkedKeys: string[] = [];
    nodes.forEach((node) => {
      selectedStaffs.forEach((staff) => {
        if (node.nodeKey.includes(`${separator}${staff?.ext_id}`)) {
          checkedKeys.push(node.key);
        }
      });
    });
    checkedKeys = _.uniq<string>(checkedKeys);
    setCheckedNodeKeys(checkedKeys);

    setCheckAll(false);
    setStaffNodes(nodes);
    setStaffTree(tree);
  }, [allStaffs, keyword]);

  // @ts-ignore
  return (
    <Modal
      width={665}
      className={'dialog from-item-label-100w'}
      visible={visible}
      zIndex={1001}
      onCancel={() => setVisible(false)}
      onOk={() => {
        if (onFinish) {
          onFinish(selectedStaffs);
        }
        setVisible(false);
      }}
    >
      <h2 className='dialog-title'> 选择员工 </h2>
      <div className={styles.addStaffDialogContent}>
        <div className={styles.container}>
          <div className={styles.left}>
            <p className={styles.toolTop} style={{ marginBottom: 0 }}>
              <Search
                className={styles.searchInput}
                enterButton={'搜索'}
                prefix={<SearchOutlined />}
                placeholder='请输入员工昵称'
                allowClear
                value={keyword}
                onChange={(e) => {
                  setKeyword(e.target.value);
                }}
              />
            </p>
            <p style={{ marginBottom: 0 }}>
              <Checkbox checked={checkAll} onChange={onCheckAllChange}>
                全部成员({staffs.length}):
              </Checkbox>
              <Button
                type={'link'}
                onClick={() => {
                  const currentStatus = !expandAll;
                  if (currentStatus) {
                    setExpandedNodeKeys(
                      _.map(
                        staffNodes.filter((staffNode) => staffNode.type === 'group'),
                        'key',
                      ),
                    );
                  } else {
                    setExpandedNodeKeys([rootNode]);
                  }
                  setExpandAll(currentStatus);
                }}
                style={{ marginRight: 30 }}
              >
                {!expandAll ? '展开部门' : '收起部门'}
              </Button>
            </p>
            <div className={styles.allStaff}>
              {staffTree.length === 0 && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
              <Tree
                className={styles.staffTree}
                autoExpandParent={true}
                checkedKeys={checkedNodeKeys}
                defaultExpandedKeys={checkedNodeKeys}
                expandedKeys={expandedNodeKeys}
                // @ts-ignore
                onExpand={(expandedKeys: string[]) => {
                  setExpandedNodeKeys(expandedKeys);
                }}
                height={300}
                switcherIcon={<CaretDownFilled style={{ color: '#47a7ff' }} />}
                checkable={true}
                multiple={true}
                treeData={staffTree}
                // @ts-ignore
                onCheck={onNodesCheck}
                titleRender={nodeRender}
              />
            </div>
          </div>
          <div className={styles.right}>
            <p>
              已选成员({selectedStaffs.length}):
              <Button
                type={'link'}
                onClick={() => {
                  setSelectedStaffs([]);
                  setCheckAll(false);
                }}
              >
                清空
              </Button>
            </p>
            <ul className={styles.allStaffList}>
              {selectedStaffs.map((staff) => {
                if (!staff?.ext_id) {
                  return '';
                }

                return (
                  <li
                    key={staff?.ext_id}
                    onClick={() => {
                      setSelectedStaffs(
                        selectedStaffs.filter((item) => item?.ext_id !== staff?.ext_id),
                      );
                    }}
                  >
                    <div className={styles.avatarAndName}>
                      <img src={staff?.avatar_url} className={styles.avatar} />
                      <div className='flex-col align-left'>
                        <span>{staff?.name}</span>
                      </div>
                    </div>
                    <CloseOutlined />
                  </li>
                );
              })}
            </ul>
          </div>
        </div>
      </div>
    </Modal>
  );
}
Example #22
Source File: index.tsx    From datart with Apache License 2.0 4 votes vote down vote up
export function ListTitle({
  title,
  subTitle,
  search,
  add,
  more,
  back,
  className,
  onSearch,
  onPrevious,
  onNext,
}: ListTitleProps) {
  const [searchbarVisible, setSearchbarVisible] = useState(false);
  const t = useI18NPrefix('components.listTitle');
  const toggleSearchbar = useCallback(() => {
    setSearchbarVisible(!searchbarVisible);
  }, [searchbarVisible]);

  const moreMenuClick = useCallback(
    ({ key }) => {
      more?.callback(key, onPrevious, onNext);
    },
    [more, onPrevious, onNext],
  );

  const backClick = useCallback(() => {
    onPrevious && onPrevious();
  }, [onPrevious]);

  return (
    <Wrapper className={className}>
      <Title className="title">
        {back && (
          <span className="back" onClick={backClick}>
            <LeftOutlined />
          </span>
        )}
        {title && <h3>{title}</h3>}
        {subTitle && <h5>{subTitle}</h5>}
        <Space size={SPACE_UNIT}>
          {search && (
            <Tooltip title={t('search')} placement="bottom">
              <ToolbarButton
                size="small"
                icon={<SearchOutlined />}
                color={searchbarVisible ? PRIMARY : void 0}
                onClick={toggleSearchbar}
              />
            </Tooltip>
          )}
          {add && <AddButton dataSource={add} />}
          {more && (
            <Popup
              getPopupContainer={triggerNode =>
                triggerNode.parentElement as HTMLElement
              }
              trigger={['click']}
              placement="bottomRight"
              content={
                <MenuWrapper
                  prefixCls="ant-dropdown-menu"
                  selectable={false}
                  onClick={moreMenuClick}
                >
                  {more.items.map(({ key, text, prefix, suffix }) => (
                    <MenuListItem
                      key={key}
                      {...(prefix && { prefix })}
                      {...(suffix && { suffix })}
                    >
                      {text}
                    </MenuListItem>
                  ))}
                </MenuWrapper>
              }
            >
              <ToolbarButton size="small" icon={<MoreOutlined />} />
            </Popup>
          )}
        </Space>
      </Title>
      <Searchbar visible={searchbarVisible}>
        <Input
          className="search-input"
          prefix={<SearchOutlined className="icon" />}
          placeholder={t('searchValue')}
          bordered={false}
          onChange={onSearch}
        />
      </Searchbar>
    </Wrapper>
  );
}
Example #23
Source File: index.tsx    From nanolooker with MIT License 4 votes vote down vote up
Search = ({ isHome = false }) => {
  const { t } = useTranslation();
  const { theme } = React.useContext(PreferencesContext);
  const { knownAccounts } = React.useContext(KnownAccountsContext);
  const { bookmarks } = React.useContext(BookmarksContext);
  const hasAccountBookmarks = !!Object.keys(bookmarks?.account || {}).length;
  const [isExpanded, setIsExpanded] = React.useState(isHome);
  const [isError, setIsError] = React.useState(false);
  const [filteredResults, setFilteredResults] = React.useState([] as any);
  const { searchValue, setSearchValue } = useSearch();
  const [accountBookmarks, setAccountBookmarks] = React.useState<
    { alias: string; account: string }[]
  >([]);
  const {
    searchHistory,
    addSearchHistory,
    removeSearchHistory,
  } = useSearchHistory();
  const searchRef = React.useRef(null);
  const [isOpen, setIsOpen] = React.useState(false);
  const [invalidQrCode, setInvalidQrCode] = React.useState("");

  let history = useHistory();

  const validateSearch = React.useCallback(
    async (value: any) => {
      if (!value) {
        setIsError(false);
        setFilteredResults([]);
      } else {
        const isValidAccount = isValidAccountAddress(value);
        const isValidBlock = isValidBlockHash(value);

        setIsError(!isValidAccount && !isValidBlock && value.length > 30);

        if (isValidBlock) {
          addSearchHistory(value.toUpperCase());
          history.push(`/block/${value.toUpperCase()}`);
        } else if (isValidAccount) {
          let account = getPrefixedAccount(value);

          setSearchValue(account);
          addSearchHistory(account);
          history.push(`/account/${account}`);
        } else {
          const filteredKnownAccounts = knownAccounts
            .filter(({ alias }) =>
              alias.toLowerCase().includes(value.toLowerCase()),
            )
            .map(item => renderItem(item));

          const filteredAccountBookmarks = accountBookmarks
            .filter(({ alias }) =>
              alias.toLowerCase().includes(value.toLowerCase()),
            )
            .map(item => renderItem(item as KnownAccount));

          setFilteredResults(
            filteredAccountBookmarks.concat(filteredKnownAccounts),
          );
        }
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [addSearchHistory, knownAccounts, accountBookmarks],
  );

  React.useEffect(() => {
    if (hasAccountBookmarks) {
      setAccountBookmarks(
        Object.entries(bookmarks?.account).map(([account, alias]) => ({
          account,
          alias,
        })),
      );
    } else {
      setAccountBookmarks([]);
    }

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [hasAccountBookmarks]);

  React.useEffect(() => {
    validateSearch(searchValue);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [searchValue]);

  React.useEffect(() => {
    if (!isOpen) return;

    function onScanSuccess(qrMessage: any) {
      if (isValidAccountAddress(qrMessage)) {
        setIsOpen(false);
        setSearchValue(getPrefixedAccount(qrMessage));
        document.getElementById("html5-qrcode-scan-stop-btn")?.click();
      } else {
        setInvalidQrCode(qrMessage);
      }
    }

    const html5QrcodeScanner = new window.Html5QrcodeScanner(
      `qrcode-reader-search${isHome ? "-home" : ""}`,
      {
        fps: 10,
        qrbox: 250,
      },
    );
    html5QrcodeScanner.render(onScanSuccess);

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isOpen]);

  const renderItem = ({ alias, account }: KnownAccount) => ({
    value: account,
    label: (
      <>
        <strong style={{ display: "block" }}>{alias}</strong>
        {account}
      </>
    ),
  });

  return (
    <>
      <AutoComplete
        style={{
          float: isExpanded ? "right" : "none",
          maxWidth: "calc(100vw - 24px)",
          width: isExpanded ? "650px" : "100%",
          // transitionDelay: `${isExpanded ? 0 : 0.2}s`,
        }}
        dropdownClassName={`search-autocomplete-dropdown ${
          theme === Theme.DARK ? "theme-dark" : ""
        }`}
        dropdownStyle={{
          maxWidth: "calc(100vw - 40px)",
        }}
        options={filteredResults}
        // @ts-ignore
        onSelect={validateSearch}
        onChange={value => {
          setSearchValue(value);
        }}
        // @ts-ignore
        onPaste={e => {
          e.preventDefault();

          // @ts-ignore
          const paste = (e.clipboardData || window.clipboardData).getData(
            "text",
          );

          const account = getAccountAddressFromText(paste);
          const hash = getAccountBlockHashFromText(paste);
          if (account || hash) {
            setSearchValue(account || hash);
          } else {
            setSearchValue(paste);
          }
        }}
        value={searchValue}
      >
        <Input
          ref={searchRef}
          allowClear
          suffix={
            <>
              <CameraOutlined
                onClick={() => setIsOpen(true)}
                className="search-history-icon"
                style={{ padding: "6px", marginRight: "3px" }}
              />
              <Dropdown
                key="search-history-dropdown"
                overlayStyle={{ zIndex: 1050 }}
                overlayClassName={theme === Theme.DARK ? "theme-dark" : ""}
                overlay={
                  <Menu>
                    {!searchHistory.length ? (
                      <Menu.Item disabled>{t("search.noHistory")}</Menu.Item>
                    ) : (
                      searchHistory.map(history => (
                        <Menu.Item
                          onClick={() => setSearchValue(history)}
                          key={history}
                        >
                          <div
                            className="color-normal"
                            style={{
                              display: "flex",
                              alignItems: "flex-start",
                            }}
                          >
                            <div>
                              {isValidAccountAddress(history) ? (
                                <WalletOutlined />
                              ) : (
                                <BlockOutlined />
                              )}
                            </div>
                            <div
                              className="break-word"
                              style={{ margin: "0 6px", whiteSpace: "normal" }}
                            >
                              {history}
                            </div>
                            <DeleteButton
                              onClick={(e: Event) => {
                                e.stopPropagation();
                                removeSearchHistory(history);
                              }}
                            />
                          </div>
                        </Menu.Item>
                      ))
                    )}
                  </Menu>
                }
                placement="bottomRight"
              >
                <HistoryOutlined
                  className="search-history-icon"
                  style={{ padding: "6px", marginRight: "6px" }}
                />
              </Dropdown>
              <SearchOutlined />
            </>
          }
          className={`ant-input-search ${isError ? "has-error" : ""}`}
          placeholder={t("search.searchBy")}
          onFocus={({ target: { value } }) => {
            validateSearch(value);
            setIsExpanded(true);
          }}
          onBlur={() => setIsExpanded(isHome || false)}
          size={isHome ? "large" : "middle"}
          spellCheck={false}
        />
      </AutoComplete>
      <Modal
        title={t("search.scanWallet")}
        visible={isOpen}
        onCancel={() => setIsOpen(false)}
        footer={[
          <Button key="back" onClick={() => setIsOpen(false)}>
            {t("common.cancel")}
          </Button>,
        ]}
      >
        {invalidQrCode ? (
          <Alert
            message={t("pages.nanoquakejs.invalidAccount")}
            description={invalidQrCode}
            type="error"
            showIcon
            style={{ marginBottom: 12 }}
          />
        ) : null}
        <div
          id={`qrcode-reader-search${isHome ? "-home" : ""}`}
          className="qrcode-reader"
        ></div>
      </Modal>
    </>
  );
}
Example #24
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
GroupChatSelectionModal: React.FC<GroupChatSelectionProps> = (props) => {
  const { visible, setVisible, defaultCheckedGroupChats, onFinish, allGroupChats, ...rest } = props;
  const [groupChats, setGroupChats] = useState<GroupChatOption[]>([]);
  const [selectedGroupChats, setSelectedGroupChats] = useState<GroupChatOption[]>(defaultCheckedGroupChats || []);
  const [keyword, setKeyword] = useState<string>('');
  const [checkAll, setCheckAll] = useState<boolean>(false);

  const onGroupChatCheckChange = (currentGroupChat: GroupChatOption) => {
    let items = [...selectedGroupChats];
    // 群聊已被选择,则删除
    if (selectedGroupChats.find((item) => item.ext_chat_id === currentGroupChat.ext_chat_id)) {
      // 取消该群聊
      items = items.filter((item) => {
        return item.ext_chat_id !== currentGroupChat.ext_chat_id;
      });
    } else {
      // 没有被选择,则添加
      items.push(currentGroupChat);
    }
    setSelectedGroupChats(items);
  };

  const onCheckAllChange = (e: CheckboxChangeEvent) => {
    let items: GroupChatOption[];
    if (e.target.checked) {
      items = _.uniqWith<GroupChatOption>([...groupChats, ...selectedGroupChats], (a, b) => a.ext_chat_id === b.ext_chat_id);
    } else {
      // @ts-ignore
      items = _.differenceWith<GroupChatParam>(selectedGroupChats, groupChats, (a, b) => a.ext_chat_id === b.ext_chat_id);
    }
    setSelectedGroupChats(items);
    setCheckAll(e.target.checked);
  };

  useEffect(() => {
    setSelectedGroupChats(defaultCheckedGroupChats || []);
    setKeyword('');
  }, [defaultCheckedGroupChats, visible]);

  useEffect(() => {
    setGroupChats(
      allGroupChats?.filter((item) => {
        return keyword === '' || item?.label?.includes(keyword) || item?.owner_name?.includes(keyword);
      }),
    );
  }, [allGroupChats, keyword]);

  // 监听待选和选中群聊,计算全选状态
  useEffect(() => {
    let isCheckAll = true;
    const selectedGroupChatIDs = selectedGroupChats.map((selectedGroupChat) => selectedGroupChat.ext_chat_id);
    groupChats.forEach((groupChat) => {
      if (!selectedGroupChatIDs.includes(groupChat.ext_chat_id)) {
        isCheckAll = false;
      }
    });
    setCheckAll(isCheckAll);
  }, [groupChats, selectedGroupChats]);

  return (
    <Modal
      {...rest}
      width={665}
      zIndex={1001}
      className={'dialog from-item-label-100w'}
      visible={visible}
      onCancel={() => setVisible(false)}
      onOk={() => {
        if (onFinish) {
          onFinish(selectedGroupChats);
        }
        setVisible(false);
      }}
    >
      <h2 className='dialog-title'> 选择群聊 </h2>

      <div className={styles.addGroupChatDialogContent}>
        <div className={styles.container}>
          <div className={styles.left}>
            <div className={styles.toolTop}>
              <Search
                className={styles.searchInput}
                enterButton={'搜索'}
                prefix={<SearchOutlined />}
                placeholder='请输入群聊名称或群主名称'
                allowClear
                onChange={(e) => {
                  setKeyword(e.target.value);
                }}
              />
              <Checkbox checked={checkAll} onChange={onCheckAllChange}>
                全部群聊({groupChats.length}):
              </Checkbox>
            </div>
            <div className={styles.allGroupChat}>
              {groupChats.length === 0 && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
              <ul className={styles.allGroupChatList}>
                {groupChats.map((groupChat) => {
                  return (
                    <li key={groupChat.ext_chat_id}
                        onClick={() => {
                          onGroupChatCheckChange(groupChat);
                        }}
                    >
                      <div className={styles.avatarAndName}>
                        <img className={styles.avatar} src={groupChatIcon} />
                        <div className='flex-col align-left'>
                          <div className={styles.groupName}>{groupChat.name}</div>
                          <div className={styles.owner}>群主:{groupChat.owner_name}</div>
                        </div>
                      </div>

                      <Checkbox
                        checked={selectedGroupChats.find((item) => {
                          return item?.ext_chat_id === groupChat?.ext_chat_id;
                        }) !== undefined}
                      />
                    </li>
                  );
                })}
              </ul>
            </div>
          </div>
          <div className={styles.right}>
            <div className={styles.toolBar}>
              已选群聊({selectedGroupChats.length}):
              <a
                onClick={() => {
                  setSelectedGroupChats([]);
                }}
              >
                清空
              </a>
            </div>
            <ul className={styles.allGroupChatList}>
              {selectedGroupChats.map((groupChat) => {
                return (
                  <li key={groupChat.ext_chat_id}
                      onClick={() => {
                        setSelectedGroupChats(selectedGroupChats.filter((item) => item.ext_chat_id !== groupChat.ext_chat_id));
                      }}
                  >
                    <div className={styles.avatarAndName}>
                      <img className={styles.avatar} src={groupChatIcon} />
                      <div className='flex-col align-left'>
                        <div className={styles.groupName}>{groupChat.name}</div>
                        <div className={styles.owner}>群主:{groupChat.owner_name}</div>
                      </div>
                    </div>
                    <div>
                      <CloseOutlined />
                    </div>
                  </li>
                );
              })}
            </ul>
          </div>
        </div>
      </div>
    </Modal>
  );
}
Example #25
Source File: ruleModal.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
ruleModal: React.FC<props> = ({ visible, ruleModalClose, subscribe }) => {
  const { t } = useTranslation();
  const { curBusiItem } = useSelector<RootState, CommonStoreState>((state) => state.common);
  // const { busiGroups } = useSelector<RootState, CommonStoreState>((state) => state.common);
  const [busiGroups, setBusiGroups] = useState<{ id: number; name: string }[]>([]);
  const [currentStrategyDataAll, setCurrentStrategyDataAll] = useState([]);
  const [currentStrategyData, setCurrentStrategyData] = useState([]);
  const [bgid, setBgid] = useState(curBusiItem.id);
  const [query, setQuery] = useState<string>('');

  useEffect(() => {
    setBgid(curBusiItem.id);
  }, [curBusiItem]);

  useEffect(() => {
    getAlertRules();
  }, [bgid]);

  useEffect(() => {
    getTeamList('');
  }, []);

  useEffect(() => {
    filterData();
  }, [query, currentStrategyDataAll]);

  // 获取业务组列表
  const getTeamList = (query: string) => {
    console.log(111);
    let params = {
      all: 1,
      query,
      limit: 200,
    };
    getBusinessTeamList(params).then((data) => {
      setBusiGroups(data.dat || []);
    });
  };

  const debounceFetcher = useCallback(debounce(getTeamList, 400), []);

  const getAlertRules = async () => {
    const { success, dat } = await getStrategyGroupSubList({ id: bgid });
    if (success) {
      setCurrentStrategyDataAll(dat || []);
    }
  };

  const bgidChange = (val) => {
    setBgid(val);
  };

  const filterData = () => {
    const data = JSON.parse(JSON.stringify(currentStrategyDataAll));
    const res = data.filter((item) => {
      return item.name.indexOf(query) > -1 || item.append_tags.join(' ').indexOf(query) > -1;
    });
    setCurrentStrategyData(res || []);
  };

  const onSearchQuery = (e) => {
    let val = e.target.value;
    setQuery(val);
  };

  const columns: ColumnType<strategyItem>[] = [
    {
      title: t('集群'),
      dataIndex: 'cluster',
      render: (data) => {
        return <div>{data}</div>;
      },
    },
    {
      title: t('级别'),
      dataIndex: 'severity',
      render: (data) => {
        return <Tag color={priorityColor[data - 1]}>S{data}</Tag>;
      },
    },
    {
      title: t('名称'),
      dataIndex: 'name',
      render: (data, record) => {
        return (
          <div
            className='table-active-text'
            onClick={() => {
              // handleClickEdit(record.id);
            }}
          >
            {data}
          </div>
        );
      },
    },
    {
      title: t('告警接收者'),
      dataIndex: 'notify_groups_obj',
      render: (data, record) => {
        return (
          (data.length &&
            data.map(
              (
                user: {
                  nickname: string;
                  username: string;
                } & { name: string },
                index: number,
              ) => {
                return <ColorTag text={user.nickname || user.username || user.name} key={index}></ColorTag>;
              },
            )) || <div></div>
        );
      },
    },
    {
      title: t('附加标签'),
      dataIndex: 'append_tags',
      render: (data) => {
        const array = data || [];
        return (
          (array.length &&
            array.map((tag: string, index: number) => {
              return <ColorTag text={tag} key={index}></ColorTag>;
            })) || <div></div>
        );
      },
    },
    {
      title: t('更新时间'),
      dataIndex: 'update_at',
      render: (text: string) => dayjs(Number(text) * 1000).format('YYYY-MM-DD HH:mm:ss'),
    },
    {
      title: t('启用'),
      dataIndex: 'disabled',
      render: (disabled, record) => (
        <Switch
          checked={disabled === strategyStatus.Enable}
          disabled
          size='small'
          onChange={() => {
            const { id, disabled } = record;
            // updateAlertRules({
            //   ids: [id],
            //   fields: {
            //     disabled: !disabled ? 1 : 0
            //   }
            // }, curBusiItem.id
            // ).then(() => {
            //   refreshList();
            // });
          }}
        />
      ),
    },
    {
      title: t('操作'),
      dataIndex: 'operator',
      fixed: 'right',
      width: 100,
      render: (data, record) => {
        return (
          <div className='table-operator-area'>
            <div
              className='table-operator-area-normal'
              onClick={() => {
                handleSubscribe(record);
              }}
            >
              {t('订阅')}
            </div>
          </div>
        );
      },
    },
  ];

  const handleSubscribe = (record) => {
    subscribe(record);
  };

  const modalClose = () => {
    ruleModalClose();
  };

  return (
    <>
      <Modal
        title={t('订阅告警规则')}
        footer=''
        forceRender
        visible={visible}
        onCancel={() => {
          modalClose();
        }}
        width={'80%'}
      >
        <div>
          <Select
            style={{ width: '280px' }}
            value={bgid}
            onChange={bgidChange}
            showSearch
            optionFilterProp='children'
            filterOption={false}
            onSearch={(e) => debounceFetcher(e)}
            onBlur={() => getTeamList('')}
          >
            {busiGroups.map((item) => (
              <Option value={item.id} key={item.id}>
                {item.name}
              </Option>
            ))}
          </Select>
          <Input style={{ marginLeft: 10, width: '280px' }} onPressEnter={onSearchQuery} prefix={<SearchOutlined />} placeholder={t('规则名称、附加标签')} />
        </div>
        <div className='rule_modal_table'>
          <Table
            rowKey='id'
            pagination={{
              total: currentStrategyData.length,
              showQuickJumper: true,
              showSizeChanger: true,
              showTotal: (total) => {
                return `共 ${total} 条数据`;
              },
              pageSizeOptions: pageSizeOptionsDefault,
              defaultPageSize: 30,
            }}
            dataSource={currentStrategyData}
            columns={columns}
          />
        </div>
      </Modal>
    </>
  );
}
Example #26
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
DepartmentSelectionModal: React.FC<DepartmentSelectionProps> = (props) => {
  const {visible, setVisible, defaultCheckedDepartments, onFinish, allDepartments} = props;
  const [departments, setDepartments] = useState<DepartmentOption[]>([]);
  const [departmentNodes, setDepartmentNodes] = useState<TreeNode[]>([]); // 一维的节点
  const [departmentTree, setDepartmentTree] = useState<TreeNode[]>([]); // 多维的树节点
  const [selectedDepartments, setSelectedDepartments] = useState<DepartmentOption[]>(
    _.filter<DepartmentOption>(defaultCheckedDepartments) || [],
  );
  const [keyword, setKeyword] = useState<string>('');
  const [checkAll, setCheckAll] = useState<boolean>(false);
  const [expandAll, setExpandAll] = useState<boolean>(false);
  const [checkedNodeKeys, setCheckedNodeKeys] = useState<string[]>([]);
  const [expandedNodeKeys, setExpandedNodeKeys] = useState<string[]>(['1']);
  const allDepartmentMap = _.keyBy(allDepartments, 'ext_id');

  const onCheckAllChange = (e: CheckboxChangeEvent) => {
    let items: DepartmentOption[];
    if (e.target.checked) {
      items = _.uniqWith<DepartmentOption>(
        [...departments, ...selectedDepartments],
        (a, b) => a.ext_id === b.ext_id,
      );
    } else {
      items = _.differenceWith(selectedDepartments, departments, (a, b) => a.ext_id === b.ext_id);
    }
    setSelectedDepartments(items);
    setCheckAll(e.target.checked);
  };

  const onNodesCheck = (checked: { checked: string[]; halfChecked: string[] }) => {
    const checkedExtDepartmentIDs: number[] = [];
    let selectedExtDepartmentIDs = selectedDepartments.map((item) => item.ext_id);
    let checkedKeys = [...checked.checked];

    // 找出本次uncheck的key,根据这些key的ext_id去删除相关checkedKey
    const uncheckedKeys = _.difference(checkedNodeKeys, checkedKeys);
    _.forEach<string>(uncheckedKeys, (key: string) => {
      // @ts-ignore
      checkedKeys = checkedKeys.filter<string>((checkedKey) => {
        return !checkedKey.includes(key);
      });
    });

    // 记录当前所有checked的key
    checkedKeys.forEach((key) => {
      checkedExtDepartmentIDs.push(Number(key));
      selectedExtDepartmentIDs.push(Number(key));
    });

    // 计算需要删除的extDepartmentID
    // @ts-ignore
    const shouldDeleteExtDepartmentIDs = _.difference(
      _.map(departments, 'ext_id'),
      checkedExtDepartmentIDs,
    );
    selectedExtDepartmentIDs = _.difference(
      _.uniq(selectedExtDepartmentIDs),
      _.uniq(shouldDeleteExtDepartmentIDs),
    );

    const items = selectedExtDepartmentIDs.map((selectedExtDepartmentID) => {
      return allDepartmentMap[selectedExtDepartmentID];
    });

    setCheckAll(departments.length === items.length);
    setSelectedDepartments(items);
  };

  const nodeRender = (node: DataNode): ReactNode => {
    return (
      <>
        <FolderFilled
          style={{
            color: '#47a7ff',
            fontSize: 20,
            marginRight: 6,
            verticalAlign: -6,
          }}
        />
        {node.title}
      </>
    );
  };

  useEffect(() => {
    setSelectedDepartments(_.filter<DepartmentOption>(defaultCheckedDepartments) || []);
    setKeyword('');
  }, [defaultCheckedDepartments, visible]);

  // 监听选中部门变化,计算checked的树节点
  useEffect(() => {
    const allDepartmentNodeKeys = _.map(departmentNodes, 'key');
    // 计算当前选中的部门,命中的key
    const matchedKeys: string[] = [];
    allDepartmentNodeKeys.forEach((key: string) => {
      selectedDepartments.forEach((department) => {
        if (key === `${department.ext_id}`) {
          matchedKeys.push(key);
        }
      });
    });
    setCheckedNodeKeys(matchedKeys);
  }, [selectedDepartments]);

  // 关键词变化的时候
  useEffect(() => {
    let filteredDepartments: DepartmentOption[] = [];

    allDepartments.forEach((item) => {
      if (keyword.trim() === '' || item.label.includes(keyword.trim())) {
        filteredDepartments.push(item);
      }
    })

    // 把搜索结果的父级节点放进结果集,方便构造树
    const pushParentDepartment = (item: DepartmentOption) => {
      if (item.ext_parent_id === 0) {
        filteredDepartments.push(item);
        return;
      }
      const parent = allDepartmentMap[item.ext_parent_id];
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      filteredDepartments.push(parent);
      pushParentDepartment(parent);
    };

    filteredDepartments.forEach((item) => {
      pushParentDepartment(item);
    })

    filteredDepartments = _.uniq<DepartmentOption>(filteredDepartments);
    setDepartments(filteredDepartments);

    const {nodes, tree} = buildDepartmentTree(filteredDepartments);

    // 这里同步更新node节点和选中key值
    let checkedKeys: string[] = [];
    nodes.forEach((node) => {
      selectedDepartments.forEach((department) => {
        if (node.key === `${department.ext_id}`) {
          checkedKeys.push(node.key);
        }
      });
    });
    checkedKeys = _.uniq<string>(checkedKeys);
    setCheckedNodeKeys(checkedKeys);

    setCheckAll(false);
    setDepartmentNodes(nodes);
    setDepartmentTree(tree);
  }, [allDepartments, keyword]);

  // @ts-ignore
  return (
    <Modal
      width={665}
      className={'dialog from-item-label-100w'}
      visible={visible}
      zIndex={1001}
      onCancel={() => setVisible(false)}
      onOk={() => {
        if (onFinish) {
          onFinish(selectedDepartments);
        }
        setVisible(false);
      }}
    >
      <h2 className="dialog-title"> 选择部门 </h2>
      <div className={styles.addDepartmentDialogContent}>
        <div className={styles.container}>
          <div className={styles.left}>
            <p className={styles.toolTop} style={{marginBottom: 0}}>
              <Search
                className={styles.searchInput}
                enterButton={'搜索'}
                prefix={<SearchOutlined/>}
                placeholder="请输入部门名称"
                allowClear
                value={keyword}
                onChange={(e) => {
                  setKeyword(e.target.value);
                }}
              />
            </p>
            <p style={{marginBottom: 0}}>
              <Checkbox checked={checkAll} onChange={onCheckAllChange}>
                全部部门({departments.length}):
              </Checkbox>
              <Button
                type={'link'}
                onClick={() => {
                  const currentStatus = !expandAll;
                  if (currentStatus) {
                    setExpandedNodeKeys(_.map(departmentNodes, 'key'));
                  } else {
                    setExpandedNodeKeys(['0']);
                  }
                  setExpandAll(currentStatus);
                }}
                style={{marginRight: 30}}
              >
                {!expandAll ? '展开全部' : '收起全部'}
              </Button>
            </p>
            <div className={styles.allDepartment}>
              {departmentTree.length === 0 && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>}
              <Tree
                className={styles.departmentTree}
                autoExpandParent={false}
                checkStrictly={true}
                checkedKeys={checkedNodeKeys}
                defaultExpandedKeys={checkedNodeKeys}
                expandedKeys={expandedNodeKeys}
                // @ts-ignore
                onExpand={(expandedKeys: string[]) => {
                  setExpandedNodeKeys(expandedKeys);
                }}
                height={300}
                switcherIcon={<CaretDownFilled style={{color: '#47a7ff'}}/>}
                checkable={true}
                multiple={true}
                treeData={departmentTree}
                // @ts-ignore
                onCheck={onNodesCheck}
                titleRender={nodeRender}
              />
            </div>
          </div>
          <div className={styles.right}>
            <p>
              已选部门({selectedDepartments.length}):
              <Button
                type={'link'}
                onClick={() => {
                  setSelectedDepartments([]);
                  setCheckAll(false);
                }}
              >
                清空
              </Button>
            </p>
            <ul className={styles.allDepartmentList}>
              {selectedDepartments.map((department) => {
                if (!department) {
                  return <></>
                }

                return (
                  <li
                    key={department.ext_id}
                    onClick={() => {
                      setSelectedDepartments(
                        selectedDepartments.filter((item) => item.ext_id !== department.ext_id),
                      );
                    }}
                  >
                    <div className={styles.avatarAndName}>
                      <div className="flex-col align-left">
                        <FolderFilled
                          style={{
                            color: '#47a7ff',
                            fontSize: 20,
                            marginRight: 6,
                            verticalAlign: -6,
                          }}
                        />
                        {department.name}
                      </div>
                    </div>
                    <CloseOutlined/>
                  </li>
                );
              })}
            </ul>
          </div>
        </div>
      </div>
    </Modal>
  );
}
Example #27
Source File: MemberTable.tsx    From datart with Apache License 2.0 4 votes vote down vote up
MemberTable = memo(
  ({ loading, dataSource, onAdd, onChange }: MemberTableProps) => {
    const [keywords, setKeywords] = useState('');
    const [selectedRowKeys, setSelectedRowKeys] = useState<Key[]>([]);
    const t = useI18NPrefix('member.roleDetail');
    const tg = useI18NPrefix('global');

    const filteredSource = useMemo(
      () =>
        dataSource.filter(
          ({ username, email, name }) =>
            username.toLowerCase().includes(keywords) ||
            email.toLowerCase().includes(keywords) ||
            (name && name.toLowerCase().includes(keywords)),
        ),
      [dataSource, keywords],
    );

    const debouncedSearch = useMemo(() => {
      const search = e => {
        setKeywords(e.target.value);
      };
      return debounce(search, DEFAULT_DEBOUNCE_WAIT);
    }, []);

    const removeMember = useCallback(
      id => () => {
        onChange(dataSource.filter(d => d.id !== id));
        setSelectedRowKeys([]);
      },
      [dataSource, onChange],
    );

    const removeSelectedMember = useCallback(() => {
      onChange(dataSource.filter(d => !selectedRowKeys.includes(d.id)));
      setSelectedRowKeys([]);
    }, [dataSource, selectedRowKeys, onChange]);

    const columns = useMemo(
      () => [
        { dataIndex: 'username', title: t('username') },
        { dataIndex: 'email', title: t('email') },
        { dataIndex: 'name', title: t('name') },
        {
          title: tg('title.action'),
          width: 80,
          align: 'center' as const,
          render: (_, record) => (
            <Action onClick={removeMember(record.id)}>{t('remove')}</Action>
          ),
        },
      ],
      [removeMember, t, tg],
    );

    return (
      <>
        <Toolbar>
          <Col span={4}>
            <Button
              type="link"
              icon={<PlusOutlined />}
              className="btn"
              onClick={onAdd}
            >
              {t('addMember')}
            </Button>
          </Col>
          <Col span={14}>
            {selectedRowKeys.length > 0 && (
              <Button
                type="link"
                icon={<DeleteOutlined />}
                className="btn"
                onClick={removeSelectedMember}
              >
                {t('deleteAll')}
              </Button>
            )}
          </Col>
          <Col span={6}>
            <Input
              placeholder={t('searchMember')}
              prefix={<SearchOutlined className="icon" />}
              bordered={false}
              onChange={debouncedSearch}
            />
          </Col>
        </Toolbar>
        <Table
          rowKey="id"
          dataSource={filteredSource}
          columns={columns}
          loading={loading}
          size="small"
          rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
          bordered
        />
      </>
    );
  },
)
Example #28
Source File: index.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
Shield: React.FC = () => {
  const { t } = useTranslation();
  const history = useHistory();
  const [query, setQuery] = useState<string>('');
  const { curBusiItem } = useSelector<RootState, CommonStoreState>((state) => state.common);
  const [bgid, setBgid] = useState(undefined);
  const [clusters, setClusters] = useState<string[]>([]);
  const [currentShieldDataAll, setCurrentShieldDataAll] = useState<Array<subscribeItem>>([]);
  const [currentShieldData, setCurrentShieldData] = useState<Array<subscribeItem>>([]);
  const [loading, setLoading] = useState<boolean>(false);

  const columns: ColumnsType = [
    {
      title: t('集群'),
      dataIndex: 'cluster',
      render: (data) => {
        return <div>{data}</div>;
      },
    },
    {
      title: t('告警规则'),
      dataIndex: 'rule_name',
      render: (data) => {
        return <div>{data}</div>;
      },
    },
    {
      title: t('订阅标签'),
      dataIndex: 'tags',
      render: (text: any) => {
        return (
          <>
            {text
              ? text.map((tag, index) => {
                  return tag ? <div key={index}>{`${tag.key} ${tag.func} ${tag.func === 'in' ? tag.value.split(' ').join(', ') : tag.value}`}</div> : null;
                })
              : ''}
          </>
        );
      },
    },
    {
      title: t('告警接收组'),
      dataIndex: 'user_groups',
      render: (text: string, record: subscribeItem) => {
        return (
          <>
            {record.user_groups?.map((item) => (
              <ColorTag text={item.name} key={item.id}></ColorTag>
            ))}
          </>
        );
      },
    },

    {
      title: t('编辑人'),
      ellipsis: true,
      dataIndex: 'update_by',
    },
    {
      title: t('操作'),
      width: '128px',
      dataIndex: 'operation',
      render: (text: undefined, record: subscribeItem) => {
        return (
          <>
            <div className='table-operator-area'>
              <div
                className='table-operator-area-normal'
                style={{
                  cursor: 'pointer',
                  display: 'inline-block',
                }}
                onClick={() => {
                  curBusiItem?.id && history.push(`/alert-subscribes/edit/${record.id}`);
                }}
              >
                {t('编辑')}
              </div>
              <div
                className='table-operator-area-normal'
                style={{
                  cursor: 'pointer',
                  display: 'inline-block',
                }}
                onClick={() => {
                  curBusiItem?.id && history.push(`/alert-subscribes/edit/${record.id}?mode=clone`);
                }}
              >
                {t('克隆')}
              </div>
              <div
                className='table-operator-area-warning'
                style={{
                  cursor: 'pointer',
                  display: 'inline-block',
                }}
                onClick={() => {
                  confirm({
                    title: t('确定删除该订阅规则?'),
                    icon: <ExclamationCircleOutlined />,
                    onOk: () => {
                      dismiss(record.id);
                    },

                    onCancel() {},
                  });
                }}
              >
                {t('删除')}
              </div>
            </div>
          </>
        );
      },
    },
  ];

  useEffect(() => {
    getList();
  }, [curBusiItem]);

  useEffect(() => {
    filterData();
  }, [query, clusters, currentShieldDataAll]);

  const dismiss = (id: number) => {
    deleteSubscribes({ ids: [id] }, curBusiItem.id).then((res) => {
      refreshList();
      if (res.err) {
        message.success(res.err);
      } else {
        message.success(t('删除成功'));
      }
    });
  };

  const filterData = () => {
    const data = JSON.parse(JSON.stringify(currentShieldDataAll));
    const res = data.filter((item: subscribeItem) => {
      const tagFind = item?.tags?.find((tag) => {
        return tag.key.indexOf(query) > -1 || tag.value.indexOf(query) > -1 || tag.func.indexOf(query) > -1;
      });
      const groupFind = item?.user_groups?.find((item) => {
        return item?.name?.indexOf(query) > -1;
      });
      return (item?.rule_name?.indexOf(query) > -1 || !!tagFind || !!groupFind) && ((clusters && clusters?.indexOf(item.cluster) > -1) || clusters?.length === 0);
    });
    setCurrentShieldData(res || []);
  };

  const getList = async () => {
    if (curBusiItem.id) {
      setLoading(true);
      const { success, dat } = await getSubscribeList({ id: curBusiItem.id });
      if (success) {
        setCurrentShieldDataAll(dat || []);
        setLoading(false);
      }
    }
  };

  const refreshList = () => {
    getList();
  };

  const onSearchQuery = (e) => {
    let val = e.target.value;
    setQuery(val);
  };

  const clusterChange = (data) => {
    setClusters(data);
  };

  const busiChange = (data) => {
    setBgid(data);
  };

  return (
    <PageLayout title={t('订阅规则')} icon={<CopyOutlined />} hideCluster>
      <div className='shield-content'>
        <LeftTree
          busiGroup={{
            // showNotGroupItem: true,
            onChange: busiChange,
          }}
        ></LeftTree>
        {curBusiItem?.id ? (
          <div className='shield-index'>
            <div className='header'>
              <div className='header-left'>
                <RefreshIcon
                  className='strategy-table-search-left-refresh'
                  onClick={() => {
                    refreshList();
                  }}
                />
                <ColumnSelect onClusterChange={(e) => setClusters(e)} />
                <Input onPressEnter={onSearchQuery} className={'searchInput'} prefix={<SearchOutlined />} placeholder={t('搜索规则、标签、接收组')} />
              </div>
              <div className='header-right'>
                <Button
                  type='primary'
                  className='add'
                  ghost
                  onClick={() => {
                    history.push('/alert-subscribes/add');
                  }}
                >
                  {t('新增订阅规则')}
                </Button>
              </div>
            </div>
            <Table
              rowKey='id'
              pagination={{
                total: currentShieldData.length,
                showQuickJumper: true,
                showSizeChanger: true,
                showTotal: (total) => {
                  return `共 ${total} 条数据`;
                },
                pageSizeOptions: pageSizeOptionsDefault,
                defaultPageSize: 30,
              }}
              loading={loading}
              dataSource={currentShieldData}
              columns={columns}
            />
          </div>
        ) : (
          <BlankBusinessPlaceholder text='订阅规则' />
        )}
      </div>
    </PageLayout>
  );
}
Example #29
Source File: MailTagFormItem.tsx    From datart with Apache License 2.0 4 votes vote down vote up
MailTagFormItem: FC<MailTagFormItemProps> = ({
  value,
  onChange,
}) => {
  const [dataSource, setDataSource] = useState<IUserInfo[]>([]);
  const [keyword, setKeyword] = useState('');
  const t = useI18NPrefix(
    'main.pages.schedulePage.sidebar.editorPage.emailSettingForm.mailTagFormItem',
  );

  const emails = useMemo(() => {
    return value ? value.split(';').filter(v => !!v) : [];
  }, [value]);

  const onSearch = useCallback(async keyword => {
    if (keyword) {
      const res = await searchUserEmails(keyword);
      setDataSource(res);
    } else {
      setDataSource([]);
    }
  }, []);
  const onDebouncedSearch = useMemo(
    () => debounce(onSearch, DEFAULT_DEBOUNCE_WAIT),
    [onSearch],
  );

  const onSelectOrRemoveEmail = useCallback(
    (email: string) => {
      const _emails = [...emails];
      const index = _emails.indexOf(email);
      if (index > -1) {
        _emails.splice(index, 1);
      } else {
        _emails.push(email);
      }
      onChange?.(_emails.join(';'));
    },
    [onChange, emails],
  );

  useEffect(() => {
    setKeyword('');
  }, [value]);

  const options = useMemo(() => {
    const items = dataSource.filter(v => !emails.includes(v?.email));
    return items.map(({ id, username, email, avatar }) => (
      <Option key={id} value={email}>
        <Space>
          <Avatar src={''} size="small" icon={<UserOutlined />} />
          <span>{username}</span>
          <span>{email}</span>
        </Space>
      </Option>
    ));
  }, [dataSource, emails]);

  const appendOptions = useMemo(() => {
    const newEmail = keyword as string;
    if (
      !regexEmail.test(newEmail) ||
      ~dataSource.findIndex(({ email }) => email === newEmail) < 0
    ) {
      return [];
    }
    return [
      <Option key={newEmail} value={newEmail}>
        <Space>
          <Avatar size="small" icon={<UserOutlined />} />
          <span>{newEmail.split('@')[0]}</span>
          <span>{newEmail}</span>
        </Space>
      </Option>,
    ];
  }, [keyword, dataSource]);
  const autoCompleteOptions = useMemo(
    () => options.concat(appendOptions),
    [appendOptions, options],
  );

  return (
    <>
      {emails.map(email => (
        <EmailTag
          closable
          key={email}
          color="blue"
          onClose={() => onSelectOrRemoveEmail(email)}
        >
          {email}
        </EmailTag>
      ))}
      <AutoComplete
        value={keyword}
        onChange={setKeyword}
        dataSource={autoCompleteOptions}
        onSearch={onDebouncedSearch}
        onSelect={onSelectOrRemoveEmail}
        onBlur={() => onSearch('')}
      >
        <Input suffix={<SearchOutlined />} placeholder={t('placeholder')} />
      </AutoComplete>
    </>
  );
}