فهرست منبع

refactor NewPageInput

Yuki Takei 1 سال پیش
والد
کامیت
6c110e9081

+ 6 - 0
apps/app/src/components/TreeItem/NewPageInput/NewPageInput.module.scss

@@ -0,0 +1,6 @@
+@use '../tree-item-variables';
+
+.new-page-input-container {
+  width: calc(100% - tree-item-variables.$btn-triangle-min-width);
+  padding-left: 24px;
+}

+ 0 - 79
apps/app/src/components/TreeItem/NewPageInput/NewPageInput.tsx

@@ -1,79 +0,0 @@
-import React, {
-  type FC, useCallback,
-} from 'react';
-
-import nodePath from 'path';
-
-import { pathUtils, pagePathUtils } from '@growi/core/dist/utils';
-import { useTranslation } from 'next-i18next';
-
-import { ValidationTarget } from '~/client/util/use-input-validator';
-import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
-import ClosableTextInput from '~/components/Common/ClosableTextInput';
-import type { IPageForItem } from '~/interfaces/page';
-
-import { NotDraggableForClosableTextInput } from '../NotDraggableForClosableTextInput';
-
-type Props = {
-  page: IPageForItem,
-  isEnableActions: boolean,
-  onSubmit?: (newPagePath: string) => Promise<void>,
-  onSubmittionFailed?: () => void,
-  onCanceled?: () => void,
-};
-
-export const NewPageInput: FC<Props> = (props) => {
-  const { t } = useTranslation();
-
-  const {
-    page, isEnableActions,
-    onSubmit, onSubmittionFailed,
-    onCanceled,
-  } = props;
-
-  const cancel = useCallback(() => {
-    onCanceled?.();
-  }, [onCanceled]);
-
-  const create = useCallback(async(inputText) => {
-    if (inputText.trim() === '') {
-      return cancel();
-    }
-
-    const parentPath = pathUtils.addTrailingSlash(page.path as string);
-    const newPagePath = nodePath.resolve(parentPath, inputText);
-    const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
-
-    if (!isCreatable) {
-      toastWarning(t('you_can_not_create_page_with_this_name'));
-      return;
-    }
-
-    try {
-      await onSubmit?.(newPagePath);
-      toastSuccess(t('successfully_saved_the_page'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-    finally {
-      onSubmittionFailed?.();
-    }
-  }, [cancel, onSubmit, onSubmittionFailed, page.path, t]);
-
-  return (
-    <>
-      {isEnableActions && (
-        <NotDraggableForClosableTextInput>
-          <ClosableTextInput
-            placeholder={t('Input page name')}
-            onPressEnter={create}
-            onPressEscape={cancel}
-            onBlur={create}
-            validationTarget={ValidationTarget.PAGE}
-          />
-        </NotDraggableForClosableTextInput>
-      )}
-    </>
-  );
-};

+ 102 - 42
apps/app/src/components/TreeItem/NewPageInput/use-new-page-input.tsx

@@ -1,8 +1,21 @@
-import React, { useState, type FC, useCallback } from 'react';
+import type { ChangeEvent } from 'react';
+import React, {
+  useState, type FC, useCallback, useRef,
+} from 'react';
+
+import nodePath from 'path';
 
 import { Origin } from '@growi/core';
+import { pathUtils, pagePathUtils } from '@growi/core/dist/utils';
+import { useRect } from '@growi/ui/dist/utils';
+import { useTranslation } from 'next-i18next';
+import { debounce } from 'throttle-debounce';
 
 import { createPage } from '~/client/services/create-page';
+import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
+import type { InputValidationResult } from '~/client/util/use-input-validator';
+import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
+import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '~/components/Common/SubmittableInput';
 import { mutatePageTree } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 
@@ -10,7 +23,9 @@ import { shouldCreateWipPage } from '../../../utils/should-create-wip-page';
 import type { TreeItemToolProps } from '../interfaces';
 
 import { NewPageCreateButton } from './NewPageCreateButton';
-import { NewPageInput } from './NewPageInput';
+
+
+import newPageInputStyles from './NewPageInput.module.scss';
 
 
 type UseNewPageInput = {
@@ -24,26 +39,15 @@ export const useNewPageInput = (): UseNewPageInput => {
   const [showInput, setShowInput] = useState(false);
   const [isProcessingSubmission, setProcessingSubmission] = useState(false);
 
-  const { getDescCount } = usePageTreeDescCountMap();
-
   const CreateButton: FC<TreeItemToolProps> = (props) => {
 
     const { itemNode, stateHandlers } = props;
-    const { page, children } = itemNode;
-
-    // descendantCount
-    const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
-
-    const isChildrenLoaded = children?.length > 0;
-    const hasDescendants = descendantCount > 0 || isChildrenLoaded;
+    const { page } = itemNode;
 
     const onClick = useCallback(() => {
       setShowInput(true);
-
-      if (hasDescendants) {
-        stateHandlers?.setIsOpen(true);
-      }
-    }, [hasDescendants, stateHandlers]);
+      stateHandlers?.setIsOpen(true);
+    }, [stateHandlers]);
 
     return (
       <NewPageCreateButton
@@ -55,7 +59,9 @@ export const useNewPageInput = (): UseNewPageInput => {
 
   const Input: FC<TreeItemToolProps> = (props) => {
 
-    const { itemNode, stateHandlers } = props;
+    const { t } = useTranslation();
+
+    const { itemNode, stateHandlers, isEnableActions } = props;
     const { page, children } = itemNode;
 
     const { getDescCount } = usePageTreeDescCountMap();
@@ -64,41 +70,95 @@ export const useNewPageInput = (): UseNewPageInput => {
     const isChildrenLoaded = children?.length > 0;
     const hasDescendants = descendantCount > 0 || isChildrenLoaded;
 
-    const submitHandler = useCallback(async(newPagePath: string) => {
+    const parentRef = useRef<HTMLDivElement>(null);
+    const [parentRect] = useRect(parentRef);
+
+    const [validationResult, setValidationResult] = useState<InputValidationResult>();
+
+    const inputValidator = useInputValidator(ValidationTarget.PAGE);
+
+    const changeHandler = useCallback(async(e: ChangeEvent<HTMLInputElement>) => {
+      const validationResult = inputValidator(e.target.value);
+      setValidationResult(validationResult ?? undefined);
+    }, [inputValidator]);
+    const changeHandlerDebounced = debounce(300, changeHandler);
+
+    const cancel = useCallback(() => {
+      setValidationResult(undefined);
+      setShowInput(false);
+    }, []);
+
+    const create = useCallback(async(inputText) => {
+      if (inputText.trim() === '') {
+        return cancel();
+      }
+
+      const parentPath = pathUtils.addTrailingSlash(page.path as string);
+      const newPagePath = nodePath.resolve(parentPath, inputText);
+      const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
+
+      if (!isCreatable) {
+        toastWarning(t('you_can_not_create_page_with_this_name'));
+        return;
+      }
+
       setProcessingSubmission(true);
 
       setShowInput(false);
 
-      await createPage({
-        path: newPagePath,
-        body: undefined,
-        // keep grant info undefined to inherit from parent
-        grant: undefined,
-        grantUserGroupIds: undefined,
-        origin: Origin.View,
-        wip: shouldCreateWipPage(newPagePath),
-      });
+      try {
+        await createPage({
+          path: newPagePath,
+          body: undefined,
+          // keep grant info undefined to inherit from parent
+          grant: undefined,
+          grantUserGroupIds: undefined,
+          origin: Origin.View,
+          wip: shouldCreateWipPage(newPagePath),
+        });
 
-      mutatePageTree();
+        mutatePageTree();
 
-      if (!hasDescendants) {
-        stateHandlers?.setIsOpen(true);
+        if (!hasDescendants) {
+          stateHandlers?.setIsOpen(true);
+        }
+
+        toastSuccess(t('successfully_saved_the_page'));
+      }
+      catch (err) {
+        toastError(err);
       }
-    }, [hasDescendants, stateHandlers]);
+      finally {
+        setProcessingSubmission(false);
+      }
+    }, [cancel, hasDescendants, page.path, stateHandlers, t]);
 
-    const submittionFailedHandler = useCallback(() => {
-      setProcessingSubmission(false);
-    }, []);
+    const inputContainerClass = newPageInputStyles['new-page-input-container'] ?? '';
+    const isInvalid = validationResult != null;
+
+    const maxWidth = parentRect != null
+      ? getAdjustedMaxWidthForAutosizeInput(parentRect.width, 'sm', validationResult != null ? false : undefined)
+      : undefined;
 
-    return showInput
+    return isEnableActions && showInput
       ? (
-        <NewPageInput
-          page={page}
-          isEnableActions={props.isEnableActions}
-          onSubmit={submitHandler}
-          onSubmittionFailed={submittionFailedHandler}
-          onCanceled={() => setShowInput(false)}
-        />
+        <div ref={parentRef} className={inputContainerClass}>
+          <AutosizeSubmittableInput
+            inputClassName={`form-control ${isInvalid ? 'is-invalid' : ''}`}
+            inputStyle={{ maxWidth }}
+            placeholder={t('Input page name')}
+            aria-describedby={isInvalid ? 'new-page-input-feedback' : undefined}
+            onChange={changeHandlerDebounced}
+            onSubmit={create}
+            onCancel={cancel}
+            autoFocus
+          />
+          { isInvalid && (
+            <div id="new-page-input" className="invalid-feedback d-block my-1">
+              {validationResult.message}
+            </div>
+          ) }
+        </div>
       )
       : <></>;
   };