Просмотр исходного кода

Merge branch 'feat/pt-dev-4' into dev/5.0.x

Taichi Masuyama 4 лет назад
Родитель
Сommit
a12ad06dc6

+ 103 - 0
packages/app/src/components/Common/ClosableTextInput.tsx

@@ -0,0 +1,103 @@
+import React, {
+  FC, memo, useEffect, useRef, useState,
+} from 'react';
+
+export const AlertType = {
+  WARNING: 'warning',
+  ERROR: 'error',
+} as const;
+
+export type AlertType = typeof AlertType[keyof typeof AlertType];
+
+export type AlertInfo = {
+  type?: AlertType
+  message?: string
+}
+
+type ClosableTextInputProps = {
+  isShown: boolean
+  placeholder?: string
+  inputValidator?(text: string): AlertInfo | Promise<AlertInfo> | null
+  onPressEnter?(): void
+  onClickOutside?(): void
+}
+
+const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
+  const inputRef = useRef<HTMLInputElement>(null);
+
+  const [currentAlertInfo, setAlertInfo] = useState<AlertInfo | null>(null);
+
+  const onChangeHandler = async(e) => {
+    if (props.inputValidator == null) { return }
+
+    const alertInfo = await props.inputValidator(e.target.value);
+
+    setAlertInfo(alertInfo);
+  };
+
+  const onPressEnter = () => {
+    if (props.onPressEnter == null) {
+      return;
+    }
+
+    props.onPressEnter();
+  };
+
+  const onKeyDownHandler = (e) => {
+    switch (e.key) {
+      case 'Enter':
+        onPressEnter();
+        break;
+      default:
+        break;
+    }
+  };
+
+  /*
+   * Hide when click outside the ref
+   */
+  const onBlurHandler = () => {
+    if (props.onClickOutside == null) {
+      return;
+    }
+
+    props.onClickOutside();
+  };
+
+  // didMount
+  useEffect(() => {
+    // autoFocus
+    if (inputRef?.current == null) {
+      return;
+    }
+    inputRef.current.focus();
+  });
+
+
+  // TODO: improve style
+  return (
+    <div className={props.isShown ? 'd-block' : 'd-none'}>
+      <input
+        ref={inputRef}
+        type="text"
+        className="form-control"
+        placeholder={props.placeholder}
+        name="input"
+        onChange={onChangeHandler}
+        onKeyDown={onKeyDownHandler}
+        onBlur={onBlurHandler}
+        autoFocus={false}
+      />
+      <div>
+        {currentAlertInfo != null && (
+          <p>
+            {/* eslint-disable-next-line max-len */}
+            {currentAlertInfo.type != null ? currentAlertInfo.type : AlertType.ERROR}: {currentAlertInfo.message != null ? currentAlertInfo.message : 'Invalid value' }
+          </p>
+        )}
+      </div>
+    </div>
+  );
+});
+
+export default ClosableTextInput;

+ 31 - 9
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -2,11 +2,12 @@ import React, {
   useCallback, useState, FC, useEffect, memo,
 } from 'react';
 import nodePath from 'path';
+import { useTranslation } from 'react-i18next';
 
 import { ItemNode } from './ItemNode';
 import { useSWRxPageChildren } from '../../../stores/page-listing';
 import { usePageId } from '../../../stores/context';
-import { useCreateModalStatus } from '../../../stores/ui';
+import ClosableTextInput, { AlertInfo, AlertType } from '../../Common/ClosableTextInput';
 
 
 interface ItemProps {
@@ -27,12 +28,12 @@ const markTarget = (children: ItemNode[], targetId: string): void => {
 };
 
 type ItemControlProps = {
-  onClickOpenModalButtonHandler?(): void
+  onClickPlusButtonHandler?(): void
 }
 
 const ItemControl: FC<ItemControlProps> = memo((props: ItemControlProps) => {
   const onClickHandler = () => {
-    const { onClickOpenModalButtonHandler: handler } = props;
+    const { onClickPlusButtonHandler: handler } = props;
     if (handler == null) {
       return;
     }
@@ -71,6 +72,7 @@ const ItemCount: FC = () => {
 };
 
 const Item: FC<ItemProps> = (props: ItemProps) => {
+  const { t } = useTranslation();
   const { itemNode, isOpen: _isOpen = false } = props;
 
   const { page, children } = itemNode;
@@ -78,11 +80,11 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const [currentChildren, setCurrentChildren] = useState(children);
   const [isOpen, setIsOpen] = useState(_isOpen);
 
+  const [isNewPageInputShown, setNewPageInputShown] = useState(false);
+
   const { data: targetId } = usePageId();
   const { data, error } = useSWRxPageChildren(isOpen ? page._id : null);
 
-  const { open: openCreateModal } = useCreateModalStatus();
-
   const hasChildren = useCallback((): boolean => {
     return currentChildren != null && currentChildren.length > 0;
   }, [currentChildren]);
@@ -91,9 +93,21 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     setIsOpen(!isOpen);
   }, [isOpen]);
 
-  const onClickOpenModalButtonHandler = useCallback(() => {
-    openCreateModal(page.path);
-  }, [openCreateModal, page]);
+  const inputValidator = (title: string | null): AlertInfo | null => {
+    if (title == null || title === '') {
+      return {
+        type: AlertType.ERROR,
+        message: t('Page title is required'),
+      };
+    }
+
+    return null;
+  };
+
+  // TODO: go to create page page
+  const onPressEnterHandler = () => {
+    console.log('Enter key was pressed!');
+  };
 
   // didMount
   useEffect(() => {
@@ -144,9 +158,17 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           <ItemCount />
         </div>
         <div className="grw-pagetree-control d-none">
-          <ItemControl onClickOpenModalButtonHandler={onClickOpenModalButtonHandler} />
+          <ItemControl onClickPlusButtonHandler={() => { setNewPageInputShown(true) }} />
         </div>
       </div>
+
+      <ClosableTextInput
+        isShown={isNewPageInputShown}
+        placeholder={t('Input title')}
+        onClickOutside={() => { setNewPageInputShown(false) }}
+        onPressEnter={onPressEnterHandler}
+        inputValidator={inputValidator}
+      />
       {
         isOpen && hasChildren() && currentChildren.map(node => (
           <Item