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

Merge pull request #8470 from weseek/fix/page-path-header

fix: PageHeader behavior and position
Yuki Takei 2 лет назад
Родитель
Сommit
455d652b4b

+ 5 - 2
apps/app/src/components/Common/ClosableTextInput.tsx

@@ -14,6 +14,7 @@ type ClosableTextInputProps = {
   placeholder?: string
   validationTarget?: string,
   useAutosizeInput?: boolean
+  inputClassName?: string,
   onPressEnter?(inputText: string | null): void
   onPressEscape?: () => void
   onClickOutside?(): void
@@ -132,11 +133,13 @@ const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextI
     onBlur: onBlurHandler,
   };
 
+  const inputClassName = `form-control ${props.inputClassName ?? ''}`;
+
   return (
     <div>
       { props.useAutosizeInput
-        ? <AutosizeInput {...inputProps} />
-        : <input className="form-control" {...inputProps} />
+        ? <AutosizeInput inputClassName={inputClassName} {...inputProps} />
+        : <input className={inputClassName} {...inputProps} />
       }
       {isAbleToShowAlert && <AlertInfo />}
     </div>

+ 1 - 1
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -439,7 +439,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   return (
     <div data-testid="page-editor" id="page-editor" className={`flex-expand-vert ${props.visibility ? '' : 'd-none'}`}>
-      <div className="ms-3 mt-2">
+      <div className="px-4 py-2">
         <PageHeader />
       </div>
       <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>

+ 0 - 12
apps/app/src/components/PageHeader/PageHeader.module.scss

@@ -1,14 +1,2 @@
 .page-header :global {
-  .page-title-header-input {
-    input {
-      font-size: 2rem;
-    }
-  }
-
-  .page-path-header-buttons {
-    button {
-      width: 25px;
-      height: 20px;
-    }
-  }
 }

+ 3 - 1
apps/app/src/components/PageHeader/PageHeader.tsx

@@ -7,6 +7,7 @@ import { PageTitleHeader } from './PageTitleHeader';
 
 import styles from './PageHeader.module.scss';
 
+const moduleClass = styles['page-header'] ?? '';
 
 export const PageHeader: FC = () => {
   const { data: currentPage } = useSWRxCurrentPage();
@@ -16,11 +17,12 @@ export const PageHeader: FC = () => {
   }
 
   return (
-    <div className={`${styles['page-header']}`}>
+    <div className={moduleClass}>
       <PagePathHeader
         currentPage={currentPage}
       />
       <PageTitleHeader
+        className="mt-2"
         currentPage={currentPage}
       />
     </div>

+ 19 - 0
apps/app/src/components/PageHeader/PagePathHeader.module.scss

@@ -0,0 +1,19 @@
+.page-path-header :global {
+  input {
+    min-width: 20px;
+    min-height: unset;
+    padding-top: 0;
+    padding-bottom: 0;
+    line-height: 1em;
+  }
+
+  .page-path-header-buttons {
+    height: 0;
+
+    .btn {
+      width: 24px;
+      height: 24px;
+      transform: translateY(8px);
+    }
+  }
+}

+ 46 - 34
apps/app/src/components/PageHeader/PagePathHeader.tsx

@@ -2,6 +2,8 @@ import { useState, useEffect, useCallback } from 'react';
 import type { FC } from 'react';
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
+import { DevidedPagePath } from '@growi/core/dist/models';
+import { normalizePath } from '@growi/core/dist/utils/path-utils';
 import { useTranslation } from 'next-i18next';
 
 import { ValidationTarget } from '~/client/util/input-validator';
@@ -14,7 +16,12 @@ import { PageSelectModal } from '../PageSelectModal/PageSelectModal';
 
 import { usePagePathRenameHandler } from './page-header-utils';
 
-export type Props = {
+import styles from './PagePathHeader.module.scss';
+
+const moduleClass = styles['page-path-header'];
+
+
+type Props = {
   currentPage: IPagePopulatedToShowRevision
 }
 
@@ -22,12 +29,14 @@ export const PagePathHeader: FC<Props> = (props) => {
   const { t } = useTranslation();
   const { currentPage } = props;
 
-  const currentPagePath = currentPage.path;
-  const linkedPagePath = new LinkedPagePath(currentPagePath);
+  const dPagePath = new DevidedPagePath(currentPage.path, true);
+  const parentPagePath = dPagePath.former;
+
+  const linkedPagePath = new LinkedPagePath(parentPagePath);
 
   const [isRenameInputShown, setRenameInputShown] = useState(false);
-  const [isButtonsShown, setButtonShown] = useState(false);
-  const [editedPagePath, setEditedPagePath] = useState(currentPagePath);
+  const [isHover, setHover] = useState(false);
+  const [editingParentPagePath, setEditingParentPagePath] = useState(parentPagePath);
 
   const { data: PageSelectModalData, open: openPageSelectModal } = usePageSelectModal();
   const isOpened = PageSelectModalData?.isOpened ?? false;
@@ -43,27 +52,25 @@ export const PagePathHeader: FC<Props> = (props) => {
   }, []);
 
   const onInputChange = useCallback((inputText: string) => {
-    setEditedPagePath(inputText);
+    setEditingParentPagePath(inputText);
   }, []);
 
   const onPressEnter = useCallback(() => {
-    pagePathRenameHandler(editedPagePath, onRenameFinish, onRenameFailure);
-  }, [editedPagePath, onRenameFailure, onRenameFinish, pagePathRenameHandler]);
+    const pathToRename = normalizePath(`${editingParentPagePath}/${dPagePath.latter}`);
+    pagePathRenameHandler(pathToRename, onRenameFinish, onRenameFailure);
+  }, [editingParentPagePath, onRenameFailure, onRenameFinish, pagePathRenameHandler, dPagePath.latter]);
 
   const onPressEscape = useCallback(() => {
-    setEditedPagePath(currentPagePath);
+    // reset
+    setEditingParentPagePath(parentPagePath);
     setRenameInputShown(false);
-  }, [currentPagePath]);
+  }, [parentPagePath]);
 
   const onClickEditButton = useCallback(() => {
-    if (isRenameInputShown) {
-      pagePathRenameHandler(editedPagePath, onRenameFinish, onRenameFailure);
-    }
-    else {
-      setEditedPagePath(currentPagePath);
-      setRenameInputShown(true);
-    }
-  }, [currentPagePath, editedPagePath, isRenameInputShown, onRenameFailure, onRenameFinish, pagePathRenameHandler]);
+    // reset
+    setEditingParentPagePath(parentPagePath);
+    setRenameInputShown(true);
+  }, [parentPagePath]);
 
   const clickOutSideHandler = useCallback((e) => {
     const container = document.getElementById('page-path-header');
@@ -82,47 +89,52 @@ export const PagePathHeader: FC<Props> = (props) => {
   }, [clickOutSideHandler]);
 
 
+  if (dPagePath.isRoot) {
+    return <></>;
+  }
+
   return (
     <div
       id="page-path-header"
-      className="d-flex"
-      onMouseEnter={() => setButtonShown(true)}
-      onMouseLeave={() => setButtonShown(false)}
+      className={`d-flex ${moduleClass} small`}
+      onMouseEnter={() => setHover(true)}
+      onMouseLeave={() => setHover(false)}
     >
       <div className="me-2">
-        {isRenameInputShown
-          ? (
+        { isRenameInputShown && (
+          <div className="position-absolute">
             <ClosableTextInput
               useAutosizeInput
-              value={editedPagePath}
+              value={editingParentPagePath}
               placeholder={t('Input page name')}
+              inputClassName="form-control-sm"
               onPressEnter={onPressEnter}
               onPressEscape={onPressEscape}
               onChange={onInputChange}
               validationTarget={ValidationTarget.PAGE}
             />
-          )
-          : (
-            <PagePathHierarchicalLink linkedPagePath={linkedPagePath} />
-          )
-        }
+          </div>
+        ) }
+        <div className={`${isRenameInputShown ? 'invisible' : ''}`}>
+          <PagePathHierarchicalLink linkedPagePath={linkedPagePath} />
+        </div>
       </div>
 
-      <div className={`page-path-header-buttons d-flex align-items-center ${isButtonsShown ? '' : 'd-none'}`}>
+      <div className={`page-path-header-buttons d-flex align-items-center ${isHover && !isRenameInputShown ? '' : 'invisible'}`}>
         <button
           type="button"
-          className="btn btn-sm text-muted border border-secondary me-2 d-flex align-items-center justify-content-center"
+          className="btn btn-outline-neutral-secondary me-2 d-flex align-items-center justify-content-center"
           onClick={onClickEditButton}
         >
-          <span className="material-symbols-outlined fs-5 mt-1">{isRenameInputShown ? 'check_circle' : 'edit'}</span>
+          <span className="material-symbols-outlined fs-6">edit</span>
         </button>
 
         <button
           type="button"
-          className="btn btn-sm text-muted border border-secondary d-flex align-items-center justify-content-center"
+          className="btn btn-outline-neutral-secondary d-flex align-items-center justify-content-center"
           onClick={openPageSelectModal}
         >
-          <span className="material-symbols-outlined fs-5 mt-1">account_tree</span>
+          <span className="material-symbols-outlined fs-6">account_tree</span>
         </button>
       </div>
 

+ 9 - 0
apps/app/src/components/PageHeader/PageTitleHeader.module.scss

@@ -0,0 +1,9 @@
+.page-title-header :global {
+  input {
+    min-width: 20px;
+    min-height: unset;
+    padding: 0 0.5rem;
+    line-height: 1em;
+    transform: translateX(-0.55rem) translateY(-0.05rem);
+  }
+}

+ 41 - 31
apps/app/src/components/PageHeader/PageTitleHeader.tsx

@@ -3,6 +3,8 @@ import { useState, useCallback } from 'react';
 
 import nodePath from 'path';
 
+import type { IPagePopulatedToShowRevision } from '@growi/core';
+import { DevidedPagePath } from '@growi/core/dist/models';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 
@@ -11,9 +13,16 @@ import { ValidationTarget } from '~/client/util/input-validator';
 import ClosableTextInput from '../Common/ClosableTextInput';
 import { CopyDropdown } from '../Common/CopyDropdown';
 
-import type { Props } from './PagePathHeader';
 import { usePagePathRenameHandler } from './page-header-utils';
 
+import styles from './PageTitleHeader.module.scss';
+
+const moduleClass = styles['page-title-header'];
+
+type Props = {
+  currentPage: IPagePopulatedToShowRevision,
+  className?: string,
+};
 
 export const PageTitleHeader: FC<Props> = (props) => {
   const { t } = useTranslation();
@@ -21,7 +30,8 @@ export const PageTitleHeader: FC<Props> = (props) => {
 
   const currentPagePath = currentPage.path;
 
-  const pageTitle = nodePath.basename(currentPagePath) || '/';
+  const dPagePath = new DevidedPagePath(currentPage.path, true);
+  const pageTitle = dPagePath.latter;
 
   const [isRenameInputShown, setRenameInputShown] = useState(false);
   const [editedPagePath, setEditedPagePath] = useState(currentPagePath);
@@ -61,38 +71,38 @@ export const PageTitleHeader: FC<Props> = (props) => {
 
 
   return (
-    <div className="d-flex">
+    <div className={`d-flex align-items-center ${moduleClass} ${props.className ?? ''}`}>
       <div className="me-1">
-        {isRenameInputShown
-          ? (
-            <div className="page-title-header-input">
-              <ClosableTextInput
-                useAutosizeInput
-                value={editedPageTitle}
-                placeholder={t('Input page name')}
-                onPressEnter={onPressEnter}
-                onPressEscape={onPressEscape}
-                onChange={onInputChange}
-                onClickOutside={() => setRenameInputShown(false)}
-                validationTarget={ValidationTarget.PAGE}
-              />
-            </div>
-          )
-          : (
-            <h2 onClick={onClickPageTitle}>
-              {pageTitle}
-            </h2>
-          )}
+        { isRenameInputShown && (
+          <div className="position-absolute">
+            <ClosableTextInput
+              useAutosizeInput
+              value={editedPageTitle}
+              placeholder={t('Input page name')}
+              inputClassName="fs-4"
+              onPressEnter={onPressEnter}
+              onPressEscape={onPressEscape}
+              onChange={onInputChange}
+              onClickOutside={() => setRenameInputShown(false)}
+              validationTarget={ValidationTarget.PAGE}
+            />
+          </div>
+        ) }
+        <h1 className={`mb-0 fs-4 ${isRenameInputShown ? 'invisible' : ''}`} onClick={onClickPageTitle}>
+          {pageTitle}
+        </h1>
       </div>
 
-      <CopyDropdown
-        pageId={currentPage._id}
-        pagePath={currentPage.path}
-        dropdownToggleId={`copydropdown-${currentPage._id}`}
-        dropdownToggleClassName="p-2"
-      >
-        <span className="material-symbols-outlined fs-5">content_paste</span>
-      </CopyDropdown>
+      <div className={`${isRenameInputShown ? 'invisible' : ''}`}>
+        <CopyDropdown
+          pageId={currentPage._id}
+          pagePath={currentPage.path}
+          dropdownToggleId={`copydropdown-${currentPage._id}`}
+          dropdownToggleClassName="ms-2 p-1"
+        >
+          <span className="material-symbols-outlined fs-6">content_paste</span>
+        </CopyDropdown>
+      </div>
     </div>
   );
 };

+ 2 - 2
apps/app/src/models/linked-page-path.js

@@ -8,9 +8,9 @@ const { isTrashPage } = pagePathUtils;
  */
 export default class LinkedPagePath {
 
-  constructor(path, skipNormalize = false) {
+  constructor(path) {
 
-    const pagePath = new DevidedPagePath(path, skipNormalize);
+    const pagePath = new DevidedPagePath(path);
 
     this.path = path;
     this.pathName = pagePath.latter;