WNomunomu 2 лет назад
Родитель
Сommit
0acb10589c

+ 164 - 5
apps/app/src/components/Sidebar/PageTree/PageTreeItem.tsx

@@ -4,19 +4,31 @@ import React, {
 
 import nodePath from 'path';
 
-import { pagePathUtils } from '@growi/core';
+
+import { pathUtils, pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useDrag, useDrop } from 'react-dnd';
+import { DropdownToggle } from 'reactstrap';
 
+import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
 import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastWarning, toastError } from '~/client/util/toastr';
-import { IPageHasId } from '~/interfaces/page';
+import { ValidationTarget } from '~/client/util/input-validator';
+import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
+import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
+import {
+  IPageHasId,
+  IPageInfoAll, IPageToDeleteWithMeta,
+} from '~/interfaces/page';
+import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
+import { useSWRMUTxPageInfo } from '~/stores/page';
 import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
 import loggerFactory from '~/utils/logger';
 
+import ClosableTextInput from '../../Common/ClosableTextInput';
+import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
 
 import { ItemNode } from './ItemNode';
-import SimpleItem, { SimpleItemProps } from './SimpleItem';
+import SimpleItem, { SimpleItemProps, NotDraggableForClosableTextInput, ToolOfSimpleItem } from './SimpleItem';
 
 const logger = loggerFactory('growi:cli:Item');
 
@@ -24,6 +36,148 @@ type Optional = 'itemRef' | 'itemClass' | 'mainClassName';
 
 type PageTreeItemProps = Omit<SimpleItemProps, Optional> & {key};
 
+const Ellipsis = (props) => {
+  const [isRenameInputShown, setRenameInputShown] = useState(false);
+  const { t } = useTranslation();
+
+  const {
+    page, onRenamed, onClickDuplicateMenuItem,
+    onClickDeleteMenuItem, isEnableActions, isReadOnlyUser,
+  } = props;
+
+  const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
+  const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(page._id ?? null);
+
+  const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
+    const bookmarkOperation = _newValue ? bookmark : unbookmark;
+    await bookmarkOperation(_pageId);
+    mutateCurrentUserBookmarks();
+    mutatePageInfo();
+  };
+
+  const duplicateMenuItemClickHandler = useCallback((): void => {
+    if (onClickDuplicateMenuItem == null) {
+      return;
+    }
+
+    const { _id: pageId, path } = page;
+
+    if (pageId == null || path == null) {
+      throw Error('Any of _id and path must not be null.');
+    }
+
+    const pageToDuplicate = { pageId, path };
+
+    onClickDuplicateMenuItem(pageToDuplicate);
+  }, [onClickDuplicateMenuItem, page]);
+
+  const renameMenuItemClickHandler = useCallback(() => {
+    setRenameInputShown(true);
+  }, []);
+
+  const onPressEnterForRenameHandler = async(inputText: string) => {
+    const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
+    const newPagePath = nodePath.resolve(parentPath, inputText);
+
+    if (newPagePath === page.path) {
+      setRenameInputShown(false);
+      return;
+    }
+
+    try {
+      setRenameInputShown(false);
+      await apiv3Put('/pages/rename', {
+        pageId: page._id,
+        revisionId: page.revision,
+        newPagePath,
+      });
+
+      if (onRenamed != null) {
+        onRenamed(page.path, newPagePath);
+      }
+
+      toastSuccess(t('renamed_pages', { path: page.path }));
+    }
+    catch (err) {
+      setRenameInputShown(true);
+      toastError(err);
+    }
+  };
+
+  const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
+    if (onClickDeleteMenuItem == null) {
+      return;
+    }
+
+    if (page._id == null || page.path == null) {
+      throw Error('_id and path must not be null.');
+    }
+
+    const pageToDelete: IPageToDeleteWithMeta = {
+      data: {
+        _id: page._id,
+        revision: page.revision as string,
+        path: page.path,
+      },
+      meta: pageInfo,
+    };
+
+    onClickDeleteMenuItem(pageToDelete);
+  }, [onClickDeleteMenuItem, page]);
+
+  const pathRecoveryMenuItemClickHandler = async(pageId: string): Promise<void> => {
+    try {
+      await resumeRenameOperation(pageId);
+      toastSuccess(t('page_operation.paths_recovered'));
+    }
+    catch {
+      toastError(t('page_operation.path_recovery_failed'));
+    }
+  };
+
+  return (
+    <>
+      {isRenameInputShown ? (
+        <div className="flex-fill">
+          <NotDraggableForClosableTextInput>
+            <ClosableTextInput
+              value={nodePath.basename(page.path ?? '')}
+              placeholder={t('Input page name')}
+              onClickOutside={() => { setRenameInputShown(false) }}
+              onPressEnter={onPressEnterForRenameHandler}
+              validationTarget={ValidationTarget.PAGE}
+            />
+          </NotDraggableForClosableTextInput>
+        </div>
+      ) : (
+        <ToolOfSimpleItem page={page}/>
+      )}
+      <NotAvailableForGuest>
+        <div className="grw-pagetree-control d-flex">
+          <PageItemControl
+            pageId={page._id}
+            isEnableActions={isEnableActions}
+            isReadOnlyUser={isReadOnlyUser}
+            onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
+            onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
+            onClickRenameMenuItem={renameMenuItemClickHandler}
+            onClickDeleteMenuItem={deleteMenuItemClickHandler}
+            onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
+            isInstantRename
+            // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
+            operationProcessData={page.processData}
+          >
+            {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
+            <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
+              <i id='option-button-in-page-tree' className="icon-options fa fa-rotate-90 p-1"></i>
+            </DropdownToggle>
+          </PageItemControl>
+        </div>
+      </NotAvailableForGuest>
+    </>
+  );
+};
+
 export const PageTreeItem: FC<PageTreeItemProps> = (props: PageTreeItemProps) => {
   const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string): string => {
     const pageTitle = nodePath.basename(droppedPagePath);
@@ -43,12 +197,16 @@ export const PageTreeItem: FC<PageTreeItemProps> = (props: PageTreeItemProps) =>
   };
 
   const { t } = useTranslation();
+
   const {
-    itemNode, isOpen: _isOpen = false, onRenamed,
+    itemNode, isOpen: _isOpen = false, onRenamed, onClickDuplicateMenuItem,
+    onClickDeleteMenuItem, isEnableActions, isReadOnlyUser,
   } = props;
+
   const { page } = itemNode;
   const [isOpen, setIsOpen] = useState(_isOpen);
   const [shouldHide, setShouldHide] = useState(false);
+
   const { mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
 
   const displayDroppedItemByPageId = useCallback((pageId) => {
@@ -164,6 +322,7 @@ export const PageTreeItem: FC<PageTreeItemProps> = (props: PageTreeItemProps) =>
       itemRef={itemRef}
       itemClass={PageTreeItem}
       mainClassName={mainClassName}
+      customComponent={Ellipsis}
     />
   );
 };

+ 86 - 58
apps/app/src/components/Sidebar/PageTree/SimpleItem.tsx

@@ -47,6 +47,7 @@ export type SimpleItemProps = {
   itemRef?
   itemClass?: React.FunctionComponent<SimpleItemProps>
   mainClassName?: string
+  customComponent?
 };
 
 // Utility to mark target
@@ -86,10 +87,57 @@ const markTarget = (children: ItemNode[], targetPathOrId?: Nullable<string>): vo
 type NotDraggableProps = {
   children: ReactNode,
 };
-const NotDraggableForClosableTextInput = (props: NotDraggableProps): JSX.Element => {
+export const NotDraggableForClosableTextInput = (props: NotDraggableProps): JSX.Element => {
   return <div draggable onDragStart={e => e.preventDefault()}>{props.children}</div>;
 };
 
+export const ToolOfSimpleItem = (props) => {
+  const { t } = useTranslation();
+  const router = useRouter();
+  const { getDescCount } = usePageTreeDescCountMap();
+
+  const page = props.page;
+  const pageName = nodePath.basename(page.path ?? '') || '/';
+
+  const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false;
+
+  const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
+
+  const pageTreeItemClickHandler = (e) => {
+    e.preventDefault();
+
+    if (page.path == null || page._id == null) {
+      return;
+    }
+
+    const link = pathUtils.returnPathForURL(page.path, page._id);
+
+    router.push(link);
+  };
+
+  return (
+    <>
+      {shouldShowAttentionIcon && (
+        <>
+          <i id="path-recovery" className="fa fa-warning mr-2 text-warning"></i>
+          <UncontrolledTooltip placement="top" target="path-recovery" fade={false}>
+            {t('tooltip.operation.attention.rename')}
+          </UncontrolledTooltip>
+        </>
+      )}
+      {page != null && page.path != null && page._id != null && (
+        <div className="grw-pagetree-title-anchor flex-grow-1">
+          <p onClick={pageTreeItemClickHandler} className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</p>
+        </div>
+      )}
+      {descendantCount > 0 && (
+        <div className="grw-pagetree-count-wrapper">
+          <CountBadge count={descendantCount} />
+        </div>
+      )}
+    </>
+  );
+};
 
 const SimpleItem: FC<SimpleItemProps> = (props: SimpleItemProps) => {
   const { t } = useTranslation();
@@ -322,6 +370,33 @@ const SimpleItem: FC<SimpleItemProps> = (props: SimpleItemProps) => {
     onClickDeleteMenuItem,
   };
 
+  const CustomComponent = props.customComponent;
+
+  // const ToolOfSimpleItem = () => {
+  //   return (
+  //     <>
+  //       {shouldShowAttentionIcon && (
+  //         <>
+  //           <i id="path-recovery" className="fa fa-warning mr-2 text-warning"></i>
+  //           <UncontrolledTooltip placement="top" target="path-recovery" fade={false}>
+  //             {t('tooltip.operation.attention.rename')}
+  //           </UncontrolledTooltip>
+  //         </>
+  //       )}
+  //       {page != null && page.path != null && page._id != null && (
+  //         <div className="grw-pagetree-title-anchor flex-grow-1">
+  //           <p onClick={pageTreeItemClickHandler} className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</p>
+  //         </div>
+  //       )}
+  //       {descendantCount > 0 && (
+  //         <div className="grw-pagetree-count-wrapper">
+  //           <CountBadge count={descendantCount} />
+  //         </div>
+  //       )}
+  //     </>
+  //   );
+  // };
+
   return (
     <div
       id={`pagetree-item-${page._id}`}
@@ -347,64 +422,17 @@ const SimpleItem: FC<SimpleItemProps> = (props: SimpleItemProps) => {
             </button>
           )}
         </div>
-        {isRenameInputShown
-          ? (
-            <div className="flex-fill">
-              <NotDraggableForClosableTextInput>
-                <ClosableTextInput
-                  value={nodePath.basename(page.path ?? '')}
-                  placeholder={t('Input page name')}
-                  onClickOutside={() => { setRenameInputShown(false) }}
-                  onPressEnter={onPressEnterForRenameHandler}
-                  validationTarget={ValidationTarget.PAGE}
-                />
-              </NotDraggableForClosableTextInput>
-            </div>
-          )
-          : (
-            <>
-              {shouldShowAttentionIcon && (
-                <>
-                  <i id="path-recovery" className="fa fa-warning mr-2 text-warning"></i>
-                  <UncontrolledTooltip placement="top" target="path-recovery" fade={false}>
-                    {t('tooltip.operation.attention.rename')}
-                  </UncontrolledTooltip>
-                </>
-              )}
-              {page != null && page.path != null && page._id != null && (
-                <div className="grw-pagetree-title-anchor flex-grow-1">
-                  <p onClick={pageTreeItemClickHandler} className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</p>
-                </div>
-              )}
-            </>
-          )}
-        {descendantCount > 0 && !isRenameInputShown && (
-          <div className="grw-pagetree-count-wrapper">
-            <CountBadge count={descendantCount} />
-          </div>
+
+        {CustomComponent ?? (
+          <ToolOfSimpleItem
+            page={page}
+            onRenamed={onRenamed}
+            onClickDuplicateMenuItem={onClickDuplicateMenuItem}
+            onClickDeleteMenuItem={onClickDeleteMenuItem}
+            isEnableActions={isEnableActions}
+            isReadOnlyUser={isReadOnlyUser}
+          />
         )}
-        <NotAvailableForGuest>
-          <div className="grw-pagetree-control d-flex">
-            <PageItemControl
-              pageId={page._id}
-              isEnableActions={isEnableActions}
-              isReadOnlyUser={isReadOnlyUser}
-              onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
-              onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
-              onClickRenameMenuItem={renameMenuItemClickHandler}
-              onClickDeleteMenuItem={deleteMenuItemClickHandler}
-              onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
-              isInstantRename
-              // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
-              operationProcessData={page.processData}
-            >
-              {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
-              <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
-                <i id='option-button-in-page-tree' className="icon-options fa fa-rotate-90 p-1"></i>
-              </DropdownToggle>
-            </PageItemControl>
-          </div>
-        </NotAvailableForGuest>
 
         {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
           <NotAvailableForGuest>