react-use#useClickAway TypeScript Examples

The following examples show how to use react-use#useClickAway. 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: useGlobalClickAway.ts    From UUI with MIT License 5 votes vote down vote up
export function useGlobalClickAway(active: boolean, elementRef: any, onClickAway?: (event: Event) => void) {
  const activateElement = useCallback((element: any) => {
    let foundIndex = -1
    do {
      foundIndex = GlobalActiveElements.findIndex((i) => i === element)
      if (foundIndex !== -1) GlobalActiveElements.splice(foundIndex, 1)
    } while (foundIndex !== -1);
    GlobalActiveElements.push(element)
  }, [])

  const deactivateElement = useCallback((element: any) => {
    const foundIndex = GlobalActiveElements.findIndex((i) => i === element)
    if (foundIndex === -1) return
    GlobalActiveElements.splice(foundIndex, Number.MAX_SAFE_INTEGER);
  }, [])

  const isCurrentActiveElement = useCallback((element: any) => {
    return (GlobalActiveElements.length > 0 && GlobalActiveElements[GlobalActiveElements.length-1] === element)
  }, [])

  useEffect(() => {
    if (!elementRef.current) return;
    const targetElement = elementRef.current
    if (active) {
      activateElement(targetElement)
    } else {
      deactivateElement(targetElement)
    }

    return () => {
      deactivateElement(targetElement)
    }
  }, [activateElement, deactivateElement, active, elementRef])


  useClickAway(elementRef, (event) => {
    if (active) {
      if (elementRef.current && !isCurrentActiveElement(elementRef.current)) return;
      setTimeout(() => {
        onClickAway && onClickAway(event)
      }, 0)
    }
  }, ['mouseup', 'touchend'])
}
Example #2
Source File: index.tsx    From erda-ui with GNU Affero General Public License v3.0 5 votes vote down vote up
Announcement = () => {
  const [index, setIndex] = React.useState(1);
  const [visible, setVisible] = React.useState(false);
  const ref = React.useRef(null);
  useClickAway(ref, () => {
    setVisible(false);
  });
  const announcementList = layoutStore.useStore((s) => s.announcementList);

  if (!announcementList.length) {
    return null;
  }

  const total = announcementList.length;

  const content = (
    <div ref={ref} style={{ width: 400 }} className="px-4 pb-3 rounded-sm shadow-card-lg bg-default text-white">
      <div className="h-12 flex items-center">
        <ErdaIcon type="tonggao" fill="blue" size={20} />
        <span className="ml-1">{i18n.t('layout:announcement')}</span>
      </div>
      <div className="text-white overflow-auto break-word pr-2" style={{ height: 130 }}>
        {announcementList[index - 1].content}
      </div>
      <div className="h-8 flex items-center justify-end select-none">
        <ErdaIcon
          className={`rounded-sm w-8 h-8 text-white-6 hover:text-white hover:bg-white-2 cursor-pointer`}
          type="left"
          size={20}
          onClick={() => setIndex(Math.max(index - 1, 1))}
        />
        <div className="w-12 inline-flex items-center justify-center text-white-6">
          {index} / {announcementList.length}
        </div>
        <ErdaIcon
          className={`rounded-sm w-8 h-8 text-white-6 hover:text-white hover:bg-white-2 cursor-pointer`}
          type="right"
          size={20}
          onClick={() => setIndex(Math.min(index + 1, total))}
        />
      </div>
    </div>
  );

  return (
    <Dropdown overlay={content} placement="bottomRight" visible={visible}>
      <div
        className="flex items-center px-2 h-7 cursor-pointer rounded-full bg-blue text-white"
        onClick={!visible ? () => setVisible(true) : undefined}
      >
        <ErdaIcon type="tonggao" fill="white" size={20} />
        {i18n.t('layout:announcement')}
      </div>
    </Dropdown>
  );
}
Example #3
Source File: Dropdown.tsx    From oxen-website with GNU General Public License v3.0 5 votes vote down vote up
export function Dropdown(props: Props) {
  // Ensure children are all DropdownItems
  const {
    isOpen,
    pull = 'right',
    style = 'default',
    center = false,
    offsetX,
    offsetY,
    onClickAway,
    children,
  } = props;

  const ref = useRef(null);
  useClickAway(ref, onClickAway);

  return (
    <div className="relative z-50 w-full h-0">
      <div
        style={{
          width: 'max-content',
          marginLeft: offsetX ? `${offsetX}px` : 'unset',
          marginTop: offsetY ? `${offsetY}px` : '0.5rem',
        }}
        className={classNames(
          'absolute',
          'top-0',
          'z-50',
          isOpen ? 'block' : 'hidden',
          pull === 'right' && 'left-0',
          pull === 'left' && 'right-0',
          pull === 'center' && 'left-0 right-0',
        )}
      >
        <div
          ref={ref}
          className={classNames(
            'bg-white',
            'duration-300',
            'rounded-lg',
            'transform',
            'shadow-lg',
            'overflow-hidden',
            'last:border-b-0',
            style === 'default' && ['pt-2'],
            style === 'outline' && ['py-2', 'border-2', 'border-secondary'],
          )}
        >
          {children}
        </div>
      </div>
    </div>
  );
}
Example #4
Source File: SideMenuSplit.tsx    From oxen-website with GNU General Public License v3.0 5 votes vote down vote up
export function SideMenuSplit() {
  const { pageType, sideMenuExpanded } = useSelector(
    (state: IState) => state.navigation,
  );

  const ref = useRef(null);
  const dispatch = useDispatch();

  const onClickAway = () => {
    if (sideMenuExpanded && pageType === PageType.NORMAL) {
      dispatch(collapseSideMenu());
    }
  };

  useClickAway(ref, onClickAway);

  const transform =
    pageType === PageType.NORMAL || sideMenuExpanded
      ? 'translateX(0)'
      : `translateX(-100%) translateX(${UI.SIDE_MENU_SIDE_BAR_WIDTH_PX - 3}px)`;

  return (
    <div
      ref={ref}
      style={{
        // minWidth: pageType === PageType.NORMAL ? '50vw' : '0',
        zIndex: 20033,
        height:
          pageType === PageType.NORMAL
            ? 'unset'
            : `calc(100vh - ${UI.HEADER_HEIGHT_PX}px`,
        transform,
      }}
      className={classNames(
        'relative flex text-primary bg-alt duration-300 z-50',
      )}
    >
      {pageType === PageType.NORMAL && (
        <div
          style={{
            height: `calc(100vh - ${UI.HEADER_HEIGHT_PX}px`,
          }}
          className="w-full overflow-y-auto"
        >
          <SideMenuInner />
        </div>
      )}

      <SideMenuSideBar mode={SideBarMode.LABEL} />
    </div>
  );
}
Example #5
Source File: page.tsx    From platyplus with MIT License 5 votes vote down vote up
Page: React.FC = () => {
  const { slug } = useParams()
  const { state: contents, setState: setContents } = usePage<Descendant[]>({
    slug,
    path: 'contents'
  })
  const isConfigEnabled = useConfigEnabled()
  const { state: title, setState: setTitle } = usePageTitle({ slug })
  const ref = useRef(null)
  useClickAway(ref, () => setEditing(false))
  const edit = () => isConfigEnabled && setEditing(true)
  const [editing, setEditing] = useState(false)

  return (
    <HeaderTitleWrapper
      title={title}
      component={
        <InlineValue
          editable={isConfigEnabled}
          value={title}
          onChange={setTitle}
        />
      }
    >
      <Animation.Fade in={!!contents}>
        {(props) => (
          <div {...props} ref={ref} style={{ height: '100%' }} onClick={edit}>
            {contents && (
              <RichText
                readOnly={!editing}
                value={contents}
                onChange={setContents}
              />
            )}
          </div>
        )}
      </Animation.Fade>
    </HeaderTitleWrapper>
  )
}
Example #6
Source File: ModalBase.tsx    From rcvr-app with GNU Affero General Public License v3.0 5 votes vote down vote up
ModalBase: React.FC<ModalBaseProps & Props> = ({
  open,
  onClose,
  children,
  maxWidth,
  loading,
  title,
  ...rest
}) => {
  const ref = React.useRef()
  const close = React.useCallback(() => {
    if (loading) return
    onClose()
  }, [loading, onClose])
  useClickAway(ref, close)

  React.useEffect(() => {
    if (open) document.body.classList.add('no-scroll')
    if (!open) document.body.classList.remove('no-scroll')
  }, [open])

  return (
    <AnimatePresence>
      {open && (
        <Overlay
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{ duration: 0.2 }}
          css={{ pointerEvents: open ? 'initial' : 'none' }}
        >
          <ModalBox
            ref={ref}
            {...rest}
            css={{ maxWidth }}
            initial={{ scale: 1.1 }}
            animate={{ scale: 1 }}
            exit={{ scale: 0.9 }}
          >
            <Loading show={loading} />
            <CloseButton onClose={close} />
            <Box height={4} />
            {title && (
              <Box px={10}>
                <Text variant="h3" textAlign="center">
                  {title}
                </Text>
                <Box height={6} />
              </Box>
            )}
            {children}
          </ModalBox>
        </Overlay>
      )}
    </AnimatePresence>
  )
}
Example #7
Source File: ListHeader.tsx    From nextjs-hasura-fullstack with MIT License 5 votes vote down vote up
ListHeader: React.FC<ListHeaderProps> = (props) => {
  const {
    list: { id, name, position },
  } = props

  const [updateList] = useUpdateListMutation()

  const onSubmit = async (newName: string) => {
    try {
      await updateList({
        variables: { id, name: newName, position },
      })
    } catch (err) {
      console.log(`?? [Error]: onSubmit`, Object.values(err))
    }
  }

  const [isEditting, setIsEditting] = React.useState(!name)
  const [value, setValue] = React.useState(name.toUpperCase())

  const ref = React.useRef(null)

  useClickAway(ref, () => {
    if (isEditting) {
      if (value && value !== name) {
        onSubmit(value)
      } else {
        setValue(name)
      }
      setIsEditting(false)
    }
  })

  return (
    <div className={`flex w-32 h-8`}>
      {!isEditting && (
        <Button
          isBlock
          variant="light"
          size="sm"
          className={``}
          onClick={() => {
            setIsEditting(true)
          }}
        >
          {value}
        </Button>
      )}

      {isEditting && (
        <Input
          ref={ref}
          // eslint-disable-next-line jsx-a11y/no-autofocus
          autoFocus={true}
          value={value}
          onChange={(e) => {
            setValue(e.target.value.toUpperCase())
          }}
          onKeyDown={(e) => {
            if (e.key === 'Enter') {
              onSubmit(value)
              setIsEditting(false)
            }
          }}
        />
      )}

      {/* {!!cards.length && <Badge variant="light">{cards.length}</Badge>} */}
    </div>
  )
}
Example #8
Source File: resource.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
ApiResource = (props: Merge<CP_API_RESOURCE.Props, API_SETTING.IResourceProps>) => {
  const { quotePathMap, onQuoteChange, apiName, apiDetail } = props;

  const [
    { currentMethod, apiMethodDetail, curTabKey, apiDisplayName, tempApiData, pathParams, open },
    updater,
    update,
  ] = useUpdate({
    currentMethod: API_METHODS.get as API_SETTING.ApiMethod,
    apiMethodDetail: {} as Obj,
    curTabKey: API_RESOURCE_TAB.Summary,
    apiDisplayName: '',
    tempApiData: {},
    pathParams: null,
    open: false,
  });

  const { apiData, execOperation, operations } = props?.data || {};
  const formRef = React.useRef<IFormExtendType>({} as any);

  const [openApiDoc, apiLockState, formErrorNum] = apiDesignStore.useStore((s) => [
    s.openApiDoc,
    s.apiLockState,
    s.formErrorNum,
  ]);
  const { updateOpenApiDoc, updateFormErrorNum } = apiDesignStore;

  const dataPath = React.useMemo(() => [apiName, currentMethod], [apiName, currentMethod]);

  React.useEffect(() => {
    if (apiData?.apiMethod) {
      // 适配组件化协议的内容
      updater.apiMethodDetail(apiData);
      updater.tempApiData(!isEmpty(tempApiData) ? tempApiData : apiData);
    } else {
      // 点击左侧api列表导致的内容变化,更新第一个不为空的method,更新resource内容,resource内容切换到summary
      let initialMethod = API_METHODS.get;
      some(API_METHODS, (method) => {
        if (!isEmpty(apiDetail[method])) {
          initialMethod = method;
          return true;
        } else {
          return false;
        }
      });
      updater.currentMethod(initialMethod);

      const _apiMethodDetail = apiDetail[initialMethod] || {};
      updater.apiMethodDetail(_apiMethodDetail);
    }
    updater.pathParams(null);
    updater.curTabKey(API_RESOURCE_TAB.Summary); // API切换后重置tab
  }, [apiDetail, apiData, updater, tempApiData]);

  React.useEffect(() => {
    let _name = '';
    if (apiData) {
      _name = tempApiData.apiName || apiData?.apiName;
    } else if (apiName) {
      _name = apiName;
    }
    if (_name) {
      updater.apiDisplayName(_name);
      setTimeout(() => {
        formRef.current.setFieldsValue({ apiName: _name });
      });
    }
  }, [apiData, apiName, tempApiData.apiName, updater]);

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

    const prefixPath = !apiData ? ['paths', apiName] : [];
    const tempDetail = produce(openApiDoc, (draft) => {
      if (!isEmpty(pathParams)) {
        const _pathParams = map(pathParams, (name) => {
          return {
            ...DEFAULT_PATH_PARAM,
            name,
          };
        });
        set(draft, [...prefixPath, 'parameters'], _pathParams);
      } else {
        set(draft, [...prefixPath, 'parameters'], []);
        updater.pathParams(null);
      }
    });
    updateOpenApiDoc(tempDetail);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [apiData, apiName, pathParams, updateOpenApiDoc]);

  const setFieldHandle = React.useCallback(
    (key: string, fieldData: Obj, extraProps?: Obj) => {
      const prefixPath = !apiData?.apiMethod ? ['paths', ...dataPath] : [];
      const baseFormData = !apiData?.apiMethod ? openApiDoc : tempApiData;

      if (onQuoteChange && extraProps?.typeQuotePath) {
        const newTypeQuotePath = extraProps?.typeQuotePath ? ['paths', ...dataPath, ...extraProps.typeQuotePath] : [];

        const tempQuotePathMap = produce(quotePathMap, (draft) => {
          if (extraProps?.quoteTypeName) {
            const { quoteTypeName } = extraProps;
            draft[quoteTypeName] = draft[quoteTypeName] || [];
            draft[quoteTypeName].push(newTypeQuotePath);
          }
        });
        onQuoteChange(tempQuotePathMap);
      }

      const tempDetail = produce(baseFormData, (draft) => {
        if (key !== 'responses') {
          const responseData = get(draft, [...prefixPath, 'responses']);
          // 设置默认的response
          if (!responseData) {
            set(draft, [...prefixPath, 'responses'], DEFAULT_RESPONSE);
          }

          if (key === 'summary') {
            set(draft, [...prefixPath, fieldData?.propertyName], fieldData?.propertyData);
            if (fieldData?.propertyName === 'operationId') {
              set(draft, [...prefixPath, 'summary'], fieldData?.propertyData);
            }
            if (fieldData?.newTags) {
              set(draft, 'tags', fieldData?.newTags);
              message.success(i18n.t('dop:category created successfully'));
            }
          } else if (key === 'query' || key === 'header') {
            set(draft, [...prefixPath, 'parameters'], fieldData?.parameters);
          }
        }
        if (key === 'responses' || key === 'requestBody') {
          set(draft, [...prefixPath, key], fieldData[key]);
        }
        // 设置默认的operationId
        if (!get(draft, [...prefixPath, 'operationId'])) {
          const _operationIdList: string[] = [];
          const { paths } = openApiDoc;
          forEach(keys(paths), (pathName: string) => {
            const methodData = paths[pathName];
            forEach(keys(methodData), (item) => {
              methodData[item]?.operationId && _operationIdList.push(methodData[item]?.operationId);
            });
          });
          let _operationId = 'operationId';
          while (_operationIdList.includes(_operationId)) {
            _operationId += '1';
          }
          set(draft, [...prefixPath, 'operationId'], _operationId);
        }
        // 设置默认的tags
        if (!get(draft, [...prefixPath, 'tags'])) {
          set(draft, [...prefixPath, 'tags'], ['other']);
        }
        if (!draft.tags) {
          set(draft, 'tags', [{ name: 'other' }]);
        }
      });

      if (!apiData?.apiMethod) {
        updateOpenApiDoc(tempDetail);
      } else {
        updater.tempApiData(tempDetail);
      }
    },
    [apiData, dataPath, onQuoteChange, openApiDoc, quotePathMap, tempApiData, updateOpenApiDoc, updater],
  );

  const iconClassMap = React.useMemo(() => {
    const classMap = {};
    const emptyIcon = {};
    forEach(API_METHODS, (method) => {
      const tempMethodDetail = get(openApiDoc, ['paths', apiName, method]);
      const emptyMethodClass = !tempMethodDetail || isEmpty(tempMethodDetail) ? 'btn-icon-empty' : '';
      classMap[method] = `btn-icon btn-icon-${method} ${emptyMethodClass}`;
      emptyIcon[method] = `${emptyMethodClass}`;
    });
    return { classMap, emptyIcon };
  }, [apiName, openApiDoc]);

  const deleteMethod = React.useCallback(
    (methodKey: API_METHODS) => {
      updater.open(false);
      const tempDetail = produce(openApiDoc, (draft) => {
        unset(draft, ['paths', apiName, methodKey]);
      });
      updateOpenApiDoc(tempDetail);
      if (currentMethod === methodKey) {
        updater.apiMethodDetail({});
      }
    },
    [apiName, currentMethod, openApiDoc, updateOpenApiDoc, updater],
  );

  const onApiNameChange = React.useCallback(
    (name: string) => {
      updater.apiDisplayName(name);
      props.onApiNameChange(name);

      // 获取api中的path parameters
      const _pathParams = map(name.match(pathParamReg), (item) => {
        return item.slice(1, item.length - 1);
      });
      updater.pathParams(_pathParams);

      if (onQuoteChange && !isEmpty(quotePathMap)) {
        const tempQuotePathMap = produce(quotePathMap, (draft) => {
          forEach(keys(draft), (k) => {
            forEach(draft[k], (path, i) => {
              if (path.includes(apiDisplayName)) {
                const oldPathArray = path.slice(0, path.length - 1);
                draft[k][i] = [...oldPathArray, name];
              }
            });
          });
        });
        onQuoteChange(tempQuotePathMap);
      }

      if (!apiData?.apiMethod) {
        const tempDetail = produce(openApiDoc, (draft) => {
          const apiTempData = get(draft, ['paths', apiName]);
          set(draft, ['paths', name], apiTempData);
          unset(draft, ['paths', apiName]);
        });
        updateOpenApiDoc(tempDetail);
      } else {
        const tempDetail = produce(tempApiData, (draft) => {
          set(draft, 'apiName', name);
        });
        updater.tempApiData(tempDetail);
      }
    },
    [
      apiData,
      apiDisplayName,
      apiName,
      onQuoteChange,
      openApiDoc,
      props,
      quotePathMap,
      tempApiData,
      updateOpenApiDoc,
      updater,
    ],
  );

  const hasBody = !['get', 'head'].includes(currentMethod);

  const onSaveApiData = React.useCallback(() => {
    execOperation &&
      execOperation(operations.submit, {
        apiData: { ...tempApiData, apiMethod: apiData?.apiMethod, apiName: tempApiData?.name || apiData?.apiName },
      });
  }, [apiData, execOperation, operations, tempApiData]);

  const fieldList = React.useMemo(() => {
    const existApiPathNames = keys(openApiDoc?.paths).filter((n) => n !== apiDisplayName);
    return [
      {
        type: Input,
        name: 'apiName',
        colSpan: 24,
        required: false,
        isHoldLabel: false,
        wrapperClassName: 'pl-0',
        customProps: {
          className: 'name-input',
          maxLength: INPUT_MAX_LENGTH,
          disabled: apiLockState,
          addonBefore: apiData?.apiMethod,
          placeholder: i18n.t('Please enter the {name}', { name: i18n.t('API path') }),
          onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
            const newApiName = e.target.value;

            if (newApiName && !existApiPathNames.includes(newApiName) && newApiName.startsWith('/')) {
              onApiNameChange(newApiName);
              updateFormErrorNum(0);
            } else {
              updateFormErrorNum(1);
            }
          },
        },
        rules: [
          {
            validator: (_rule: any, value: string, callback: (msg?: string) => void) => {
              if (existApiPathNames.includes(value)) {
                callback(i18n.t('the same {key} exists', { key: i18n.t('Name') }));
              } else if (!value) {
                callback(i18n.t('can not be empty'));
              } else if (!value.startsWith('/')) {
                callback(i18n.t('dop:path must start with /'));
              } else {
                callback();
              }
            },
          },
        ],
      },
    ];
  }, [apiData, apiDisplayName, apiLockState, onApiNameChange, openApiDoc, updateFormErrorNum]);

  const popconfirmRef = React.useRef(null as any);
  const selectRef = React.useRef(null) as any;

  useClickAway(selectRef, () => {
    updater.open(false);
  });

  const maskClick = React.useCallback(
    (e: any) => {
      e.stopPropagation();
      updater.open(true);
    },
    [updater],
  );

  const labelClick = React.useCallback(
    (e: any, methodKey: string) => {
      e.stopPropagation();
      updater.open(false);

      const nextHandle = () => {
        const _apiMethodDetail = get(openApiDoc, ['paths', apiName, methodKey]) || {};
        update({
          currentMethod: methodKey as API_SETTING.ApiMethod,
          curTabKey: API_RESOURCE_TAB.Summary,
          apiMethodDetail: _apiMethodDetail,
        });

        updateFormErrorNum(0);
        formRef.current.setFieldsValue({ apiName });
      };

      if (formErrorNum > 0) {
        confirm({
          title: i18n.t('dop:Are you sure to leave, with the error message not saved?'),
          onOk() {
            nextHandle();
          },
        });
      } else {
        nextHandle();
      }
    },
    [apiName, formErrorNum, openApiDoc, update, updateFormErrorNum, updater],
  );

  const renderSelectMenu = () => {
    return (
      <div className="select-container" ref={selectRef}>
        {!apiData?.apiMethod ? (
          <Select
            getPopupContainer={(triggerNode) => triggerNode.parentElement as HTMLElement}
            style={{ marginRight: '8px', width: '141px' }}
            defaultValue={currentMethod}
            open={open}
            value={currentMethod}
          >
            {map(API_METHODS, (methodKey) => {
              let item = (
                <div className="circle-container flex-all-center">
                  {iconClassMap.emptyIcon[methodKey] ? (
                    <div className={`${iconClassMap.classMap[methodKey]}`} />
                  ) : (
                    <ErdaIcon type="check" className={iconClassMap.classMap[methodKey]} />
                  )}
                </div>
              );
              if (get(openApiDoc, ['paths', apiName, methodKey])) {
                item = (
                  <Popconfirm
                    title={`${i18n.t('common:confirm to delete')}?`}
                    onConfirm={() => deleteMethod(methodKey)}
                    placement="right"
                    // disabled={apiLockState}
                    overlayClassName="popconfirm-container"
                    getPopupContainer={() => popconfirmRef?.current}
                    onCancel={(e: any) => {
                      e.stopPropagation();
                      updater.open(false);
                    }}
                  >
                    {item}
                  </Popconfirm>
                );
              }
              return (
                <Option value={methodKey} key={methodKey}>
                  <div
                    className={`api-method-option ${currentMethod === methodKey ? 'api-method-option-active' : ''}`}
                    key={methodKey}
                  >
                    <div
                      onClick={(e) => {
                        e.stopPropagation();
                        labelClick(e, methodKey);
                      }}
                    >
                      {methodKey.toUpperCase()}
                    </div>
                    {item}
                  </div>
                </Option>
              );
            })}
          </Select>
        ) : undefined}
        <div className="mask" onClick={maskClick} />
      </div>
    );
  };

  const onTabChange = (tabKey: string) => {
    const nextHandle = () => {
      updater.curTabKey(tabKey as API_RESOURCE_TAB);
      const _apiMethodDetail = get(openApiDoc, ['paths', apiName, currentMethod]) || {};
      updater.apiMethodDetail(_apiMethodDetail);
      updateFormErrorNum(0);
      formRef.current.setFieldsValue({ apiName });
    };

    if (formErrorNum > 0) {
      confirm({
        title: i18n.t('dop:Are you sure to leave, with the error message not saved?'),
        onOk() {
          nextHandle();
        },
      });
    } else {
      nextHandle();
    }
  };

  return (
    <div className="api-resource" ref={popconfirmRef}>
      <div className="popover">
        {renderSelectMenu()}
        <FormBuilder ref={formRef} className="w-full">
          <Fields fields={fieldList} />
        </FormBuilder>
      </div>

      <div className="api-resource-tabs">
        <Tabs activeKey={curTabKey} onChange={onTabChange}>
          <TabPane tab={API_RESOURCE_TAB.Summary} key={API_RESOURCE_TAB.Summary}>
            <ResourceSummary formData={apiMethodDetail} onChange={setFieldHandle} isEditMode={!apiLockState} />
          </TabPane>
          <TabPane tab={API_RESOURCE_TAB.Params} key={API_RESOURCE_TAB.Params}>
            <QueryParamsConfig
              formData={apiMethodDetail}
              paramIn="query"
              onChange={setFieldHandle}
              isEditMode={!apiLockState}
              resourceKey={curTabKey}
            />
          </TabPane>
          <TabPane tab={API_RESOURCE_TAB.Headers} key={API_RESOURCE_TAB.Headers}>
            <QueryParamsConfig
              formData={apiMethodDetail}
              paramIn="header"
              onChange={setFieldHandle}
              isEditMode={!apiLockState}
              resourceKey={curTabKey}
            />
          </TabPane>
          <TabPane tab={API_RESOURCE_TAB.Body} key={API_RESOURCE_TAB.Body} disabled={!hasBody}>
            {hasBody && (
              <ResponseConfig
                formData={apiMethodDetail}
                paramIn="requestBody"
                onChange={setFieldHandle}
                dataPath={dataPath}
                isEditMode={!apiLockState}
                resourceKey={curTabKey}
              />
            )}
          </TabPane>
          <TabPane tab={API_RESOURCE_TAB.Response} key={API_RESOURCE_TAB.Response}>
            <ResponseConfig
              formData={apiMethodDetail}
              paramIn="responses"
              onChange={setFieldHandle}
              dataPath={dataPath}
              isEditMode={!apiLockState}
              resourceKey={curTabKey}
            />
          </TabPane>
          {apiData?.apiMethod && <TabPane tab={API_RESOURCE_TAB.Test} key={API_RESOURCE_TAB.Test} />}
        </Tabs>
        {apiData?.apiMethod && (
          <div className="flex items-center flex-wrap justify-end">
            <Button type="primary" onClick={onSaveApiData}>
              {i18n.t('Save')}
            </Button>
          </div>
        )}
      </div>
    </div>
  );
}
Example #9
Source File: record-detail.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
Info = ({ appId }: { appId: string }) => {
  const [{ isExpand }, updater] = useUpdate({
    isExpand: false,
  });
  const toggleContainer: React.RefObject<HTMLDivElement> = React.useRef(null);
  const commitMsgRef: React.RefObject<HTMLDivElement> = React.useRef(null);
  const cronMsgRef: React.RefObject<HTMLDivElement> = React.useRef(null);
  const style = `main-info ${isExpand ? 'main-info-full' : ''}`;

  const [pipelineDetail] = buildStore.useStore((s) => [s.pipelineDetail]);

  useClickAway(toggleContainer, () => {
    updater.isExpand(false);
  });

  if (!pipelineDetail) return null;
  const { id: pipelineID, pipelineCron, costTimeSec = -1, commit, commitDetail } = pipelineDetail;

  const { cronExpr } = pipelineCron;
  const cronMsg = cronExpr && cronstrue.toString(cronExpr, { locale: isZh() ? 'zh_CN' : 'en' });

  const getAutoTooltipMsg = (ref: any, text: any) => {
    // show tooltip only when text overflow
    const { current = {} } = ref;
    if (current != null && current.scrollWidth > current.clientWidth) {
      return <Tooltip title={text}>{text}</Tooltip>;
    }
    return text;
  };

  const toggleExpandInfo = (event: any) => {
    event.stopPropagation();
    updater.isExpand(!isExpand);
  };

  return pipelineDetail ? (
    <div className="main-info-parent">
      <div className={style} ref={toggleContainer}>
        <Row className="mb-4">
          <Col span={12}>
            {commitDetail?.author ? <Avatar name={commitDetail.author} showName className="mb-1" size={20} /> : '-'}
            <div className="info-label">{i18n.t('Submitter')}:</div>
          </Col>
          <Col span={12}>
            <div className="nowrap" ref={commitMsgRef}>
              {getAutoTooltipMsg(commitMsgRef, replaceEmoji(commitDetail?.comment || '')) || '-'}
            </div>
            <div className="info-label">{firstCharToUpper(i18n.t('dop:commit message'))}:</div>
          </Col>
        </Row>
        <Row className="mb-4">
          <Col span={12}>
            <div className="hover-py">{commit ? <GotoCommit length={6} commitId={commit} appId={appId} /> : '-'}</div>
            <div className="info-label">{i18n.t('Commit')} ID:</div>
          </Col>
          <Col span={12}>
            {commitDetail?.time ? moment(new Date(commitDetail.time)).format('YYYY-MM-DD HH:mm:ss') : '-'}
            <div className="info-label">{i18n.t('commit date')}:</div>
          </Col>
        </Row>
        <Row className="mb-4">
          <Col span={12}>
            {costTimeSec !== -1 ? `${i18n.t('dop:time cost')} ${secondsToTime(+costTimeSec)}` : '-'}
            <div className="info-label">{i18n.t('Duration')}:</div>
          </Col>
          <Col span={12}>
            {pipelineID || '-'}
            <div className="info-label">{i18n.t('Pipeline')} ID:</div>
          </Col>
        </Row>
        <Row className="mb-4">
          {cronMsg && (
            <Col span={12}>
              <div className="nowrap" ref={cronMsgRef}>
                {getAutoTooltipMsg(cronMsgRef, cronMsg) || '-'}
              </div>
              <div className="info-label">{i18n.t('timing time')}:</div>
            </Col>
          )}
        </Row>
        <div className="trigger-btn" onClick={toggleExpandInfo}>
          {!isExpand ? (
            <ErdaIcon type="down" size="18px" className="mr-0" />
          ) : (
            <ErdaIcon type="up" size="18px" className="mr-0" />
          )}
        </div>
      </div>
    </div>
  ) : null;
}
Example #10
Source File: index.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
LandPage = () => {
  const filteredList = orgStore.useStore((s) => s.orgs);
  const loginUser = userStore.useStore((s) => s.loginUser);
  const { getJoinedOrgs } = orgStore.effects;
  const [activeOrg, setActiveOrg] = React.useState<any>(null);
  const [showOptions, setShowOptions] = React.useState(false);
  const [filterKey, setFilterKey] = React.useState('');
  const [modalVisible, setModalVisible] = React.useState(false);
  const debouncedChange = React.useRef(debounce(getJoinedOrgs, 1000));
  const ref = React.useRef(null);

  useClickAway(ref, () => {
    setShowOptions(false);
  });

  React.useEffect(() => {
    debouncedChange.current({ q: filterKey, force: true });
  }, [filterKey]);

  const getFieldsList = (form: WrappedFormUtils) => {
    const fieldsList = [
      {
        label: i18n.t('layout:Organization name'),
        name: 'displayName',
        itemProps: {
          onInput: (e: any) => {
            let v = e.target.value.trim();
            if (pinyin.isSupported()) {
              v = pinyin.convertToPinyin(v, '', true);
            }
            form.setFieldsValue({
              name: v.split(' ').join('-').toLowerCase(),
            });
            form.validateFields(['name']);
          },
        },
      },
      {
        label: i18n.t('layout:Organization identifier'),
        name: 'name',
        itemProps: {
          maxLength: 50,
        },
        rules: [
          {
            required: true,
            message: i18n.t('Please enter the {name}', { name: i18n.t('layout:Organization identifier') }),
          },
          {
            pattern: /^[a-z0-9-]*$/,
            message: i18n.t('layout:only allowed to consist of lower case characters, numbers and -'),
          },
        ],
      },
      {
        label: i18n.t('layout:org logo'),
        name: 'logo',
        required: false,
        getComp: () => <ImageUpload id="logo" form={form} showHint />,
      },
      {
        label: i18n.t('layout:Organization description'),
        name: 'desc',
        itemProps: {
          type: 'textarea',
          maxLength: 500,
        },
      },
    ];
    return fieldsList;
  };

  const onSaveOrg = (org: Partial<ORG.IOrg>) => {
    addOrg.fetch({ ...org, type: 'FREE', admins: [loginUser.id] }).then(() => {
      hideAddOrgModal();
      debouncedChange.current({ q: filterKey, force: true });
      message.success(i18n.t('layout:created successfully, please check in your org space'));
    });
  };

  const hideAddOrgModal = () => setModalVisible(false);

  return (
    <div className="land-page flex items-center justify-center h-full">
      <div className="absolute flex items-center justify-between left-20 right-20 top-5 z-10">
        <ErdaIcon className="text-white" size={60} type="erda" />
        <UserMenu placement="bottomRight" size={36} align={{ offset: [0, -6] }} className="no-arrow" />
      </div>
      <img className="bg-image" src={springBg} alt="background-image" />
      <div className="content text-white z-10">
        <div className="title">
          <div>{i18n.t('layout:On the Cloud')}</div>
          <div>{i18n.t('layout:Collaborative Application Development Platform')}</div>
        </div>
        <div className="mt-8 org-select-text">{i18n.t('layout:Choose your organization space')}</div>
        <div
          ref={ref}
          className={`mt-4 rounded-sm h-16 py-5 text-default cursor-pointer flex items-center justify-between org-select ${
            showOptions ? 'showOptions' : ''
          } ${filterKey ? 'searching' : ''}`}
        >
          <input
            className="input"
            type="text"
            value={activeOrg?.displayName || filterKey}
            onChange={(e) => !activeOrg && setFilterKey(e.target.value)}
            onClick={(e) => setShowOptions(true)}
          />
          <div className="tip text-default-6">{i18n.t('layout:Organizational space')}</div>
          <ErdaIcon className="icon mr-6" size={20} type="caret-down" />
          <div className="options">
            {filteredList.length ? (
              filteredList.map((org) => {
                return (
                  <a
                    key={org.id}
                    href={`/${org.name}`}
                    className={`option flex items-center px-2 h-[76px] cursor-pointer hover:bg-default-04 ${
                      org.id === activeOrg?.id ? 'active' : ''
                    }`}
                    onMouseEnter={() => setActiveOrg(org)}
                    onMouseLeave={() => setActiveOrg(null)}
                  >
                    {org.logo ? (
                      <img className="w-10 h-10 rounded-sm" src={org.logo} alt={`${org.name} logo`} />
                    ) : (
                      <ErdaIcon type="zuzhi-40k0k60g" size={40} />
                    )}
                    <div className="ml-2 flex-1 truncate">
                      <div className="org-name truncate">{org.displayName}</div>
                      <div className="org-sub-name text-xs text-desc truncate">{org.desc}</div>
                    </div>
                  </a>
                );
              })
            ) : (
              <div className="h-full flex-all-center">
                <ErdaIcon type="zuzhi-40k0k60g" size={64} />
                <div>
                  <div className="org-name">
                    {filterKey ? i18n.t('No matching organization') : i18n.t("Haven't join any org")}
                  </div>
                  <div className="org-sub-name text-xs text-desc">
                    {filterKey
                      ? i18n.t('Search results are empty')
                      : i18n.t('Contact your organization administrator to invite you to join')}
                  </div>
                </div>
              </div>
            )}
          </div>
        </div>
        {erdaEnv.ENABLE_APPLY_ORG && (
          <a
            className="inline-block mt-6 mr-2 px-3 leading-8 text-white bg-white-2 rounded-sm cursor-pointer hover:bg-white-4"
            href="https://www.erda.cloud/contact"
            target="_blank"
            rel="noreferrer"
          >
            {i18n.t('layout:Apply for new organization')}
          </a>
        )}
        {process.env.FOR_COMMUNITY && (
          <span
            className="inline-block mt-6 px-3 leading-8 text-white bg-white-2 rounded-sm cursor-pointer hover:bg-white-4"
            onClick={() => setModalVisible(true)}
          >
            {i18n.t('layout:Create new organization')}
          </span>
        )}
      </div>
      <FormModal
        width={600}
        name={i18n.t('org')}
        fieldsList={getFieldsList}
        modalProps={{
          destroyOnClose: true,
        }}
        visible={modalVisible}
        onOk={onSaveOrg}
        onCancel={hideAddOrgModal}
      />
    </div>
  );
}
Example #11
Source File: inline-value.tsx    From platyplus with MIT License 4 votes vote down vote up
InlineValue: React.FC<{
  editable?: boolean
  value: string
  label?: string
  onChange: (value: string) => void
}> = ({ editable = true, value, label, onChange }) => {
  // * Toggle title editing
  const [editing, toggle] = useToggle(false)
  const readValue = useMemo(() => label ?? value, [value, label])
  // * Input value state
  const [inputValue, setInputValue] = useState('')
  useEffect(() => {
    setInputValue(value)
  }, [value])

  // * Cancel title editing
  const cancel = () => {
    toggle(false)
    setInputValue(value)
  }

  // * Set title into the config store
  const validate = () => {
    toggle(false)
    onChange(inputValue)
  }

  // * If editing and clicking away, stop editing
  const ref = useRef(null)
  useClickAway(ref, cancel)

  // * Adjust Input width automatically
  // TODO width does not scale correctly with value length
  // * See useSize in react-use
  const [width, setWidth] = useState(`${14 + value?.length}ch`)
  useEffect(() => setWidth(`${14 + inputValue?.length}ch`), [inputValue])

  return (
    <Animation.Fade in={!!value}>
      {(props) => (
        <span {...props}>
          {editing ? (
            <div
              ref={ref}
              style={{
                width,
                display: 'inline-block'
              }}
            >
              <InputGroup size="xs">
                <Input
                  size="xs"
                  value={inputValue}
                  onChange={setInputValue}
                  onKeyDown={({ key }) => key === 'Enter' && validate()}
                  autoFocus
                />
                <InputGroup.Button size="xs" onClick={cancel}>
                  <Icon icon="close" />
                </InputGroup.Button>
                <InputGroup.Button size="xs" onClick={validate}>
                  <Icon icon="check" />
                </InputGroup.Button>
              </InputGroup>
            </div>
          ) : editable ? (
            <span onClick={toggle}>
              {readValue ? (
                readValue
              ) : (
                <IconButton size="xs" icon={<Icon icon="edit" />}>
                  Edit template
                </IconButton>
              )}
            </span>
          ) : (
            <span>{readValue}</span>
          )}
        </span>
      )}
    </Animation.Fade>
  )
}
Example #12
Source File: AddInput.tsx    From nextjs-hasura-fullstack with MIT License 4 votes vote down vote up
AddInput: React.FC<AddInputProps> = (props) => {
  const {
    onSubmit,
    isCreating,
    setIsCreating,
    loading,
    className,
    label,
    placeholder,
    onClickAway,
  } = props

  const [value, setValue] = React.useState('')

  const reset = React.useCallback(() => {
    setIsCreating(false)
    setValue('')
  }, [])

  const handleClickAway = React.useCallback(() => {
    onClickAway(value)
    reset()
  }, [onClickAway, reset, value])

  const ref = React.useRef<HTMLInputElement>(null)

  useClickAway(ref, () => handleClickAway())
  useKey('Escape', () => handleClickAway())

  const handleSubmit = React.useCallback(() => {
    onSubmit(value)
    reset()
  }, [onSubmit, reset, value])

  const cls = clsx(className, `flex w-full h-10`)

  return (
    <div className={cls}>
      {!isCreating && (
        <Button
          isLoading={loading}
          isBlock
          className={``}
          onClick={() => {
            setIsCreating(true)
          }}
          variant="light"
          iconLeft={
            <svg
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 20 20"
              fill="currentColor"
            >
              <path
                fillRule="evenodd"
                d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
                clipRule="evenodd"
              />
            </svg>
          }
        >
          {label}
        </Button>
      )}
      {isCreating && (
        <div className={`flex items-center justify-between`} ref={ref}>
          <Input
            // eslint-disable-next-line jsx-a11y/no-autofocus
            autoFocus={true}
            placeholder={placeholder ?? `New ${label}`}
            value={value}
            onChange={(e) => {
              setValue(e.target.value)
            }}
            className={``}
            onKeyDown={(e) => {
              if (e.key === 'Enter') {
                handleSubmit()
              }
            }}
          />
          <ButtonIcon
            className={`flex-grow ml-2`}
            size="base"
            onClick={(e) => {
              e.preventDefault()
              handleSubmit()
            }}
            icon={
              <svg
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M5 13l4 4L19 7"
                />
              </svg>
            }
          />
        </div>
      )}
    </div>
  )
}