Преглед изворни кода

Merge branch 'master' into imprv/modal-for-renaming-and-duplicating

Yuki Takei пре 4 година
родитељ
комит
3144bc8c9e

+ 1 - 0
packages/app/resource/locales/en_US/translation.json

@@ -424,6 +424,7 @@
   },
   },
   "Put Back": "Put back",
   "Put Back": "Put back",
   "Delete Completely": "Delete completely",
   "Delete Completely": "Delete completely",
+  "page_has_been_reverted": "{{path}} has been reverted",
   "modal_delete": {
   "modal_delete": {
     "delete_page": "Delete page",
     "delete_page": "Delete page",
     "deleting_page": "Deleting page",
     "deleting_page": "Deleting page",

+ 1 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -423,6 +423,7 @@
   },
   },
   "Put Back": "元に戻す",
   "Put Back": "元に戻す",
   "Delete Completely": "完全削除",
   "Delete Completely": "完全削除",
+  "page_has_been_reverted": "{{path}} を元に戻しました",
   "modal_delete": {
   "modal_delete": {
     "delete_page": "ページを削除する",
     "delete_page": "ページを削除する",
     "deleting_page": "ページパス",
     "deleting_page": "ページパス",

+ 2 - 1
packages/app/resource/locales/zh_CN/translation.json

@@ -401,7 +401,8 @@
 		}
 		}
 	},
 	},
 	"Put Back": "Put back",
 	"Put Back": "Put back",
-	"Delete Completely": "Delete completely",
+  "Delete Completely": "Delete completely",
+  "page_has_been_reverted": "{{path}} 已还原",
 	"modal_delete": {
 	"modal_delete": {
 		"delete_page": "Delete page",
 		"delete_page": "Delete page",
 		"deleting_page": "Deleting page",
 		"deleting_page": "Deleting page",

+ 5 - 5
packages/app/src/client/services/AdminHomeContainer.js

@@ -25,12 +25,12 @@ export default class AdminHomeContainer extends Container {
     this.timer = null;
     this.timer = null;
 
 
     this.state = {
     this.state = {
-      growiVersion: '',
-      nodeVersion: '',
-      npmVersion: '',
-      yarnVersion: '',
+      growiVersion: null,
+      nodeVersion: null,
+      npmVersion: null,
+      yarnVersion: null,
       copyState: this.copyStateValues.DEFAULT,
       copyState: this.copyStateValues.DEFAULT,
-      installedPlugins: [],
+      installedPlugins: null,
       isV5Compatible: null,
       isV5Compatible: null,
     };
     };
 
 

+ 9 - 3
packages/app/src/components/Admin/AdminHome/InstalledPluginTable.jsx

@@ -11,8 +11,14 @@ class InstalledPluginTable extends React.Component {
   render() {
   render() {
     const { t, adminHomeContainer } = this.props;
     const { t, adminHomeContainer } = this.props;
 
 
+    const { installedPlugins } = adminHomeContainer.state;
+
+    if (installedPlugins == null) {
+      return <></>;
+    }
+
     return (
     return (
-      <table className="table table-bordered">
+      <table data-testid="admin-installed-plugin-table" className="table table-bordered">
         <thead>
         <thead>
           <tr>
           <tr>
             <th className="text-center">{t('admin:admin_top.package_name')}</th>
             <th className="text-center">{t('admin:admin_top.package_name')}</th>
@@ -25,8 +31,8 @@ class InstalledPluginTable extends React.Component {
             return (
             return (
               <tr key={plugin.name}>
               <tr key={plugin.name}>
                 <td>{plugin.name}</td>
                 <td>{plugin.name}</td>
-                <td className="text-center">{plugin.requiredVersion}</td>
-                <td className="text-center">{plugin.installedVersion}</td>
+                <td data-hide-in-vrt className="text-center">{plugin.requiredVersion}</td>
+                <td data-hide-in-vrt className="text-center">{plugin.installedVersion}</td>
               </tr>
               </tr>
             );
             );
           })}
           })}

+ 13 - 5
packages/app/src/components/Admin/AdminHome/SystemInfomationTable.jsx

@@ -11,24 +11,32 @@ class SystemInformationTable extends React.Component {
   render() {
   render() {
     const { adminHomeContainer } = this.props;
     const { adminHomeContainer } = this.props;
 
 
+    const {
+      growiVersion, nodeVersion, npmVersion, yarnVersion,
+    } = adminHomeContainer.state;
+
+    if (growiVersion == null || nodeVersion == null || npmVersion == null || yarnVersion == null) {
+      return <></>;
+    }
+
     return (
     return (
-      <table className="table table-bordered">
+      <table data-testid="admin-system-information-table" className="table table-bordered">
         <tbody>
         <tbody>
           <tr>
           <tr>
             <th>GROWI</th>
             <th>GROWI</th>
-            <td>{ adminHomeContainer.state.growiVersion }</td>
+            <td data-hide-in-vrt>{ growiVersion }</td>
           </tr>
           </tr>
           <tr>
           <tr>
             <th>node.js</th>
             <th>node.js</th>
-            <td>{ adminHomeContainer.state.nodeVersion }</td>
+            <td>{ nodeVersion }</td>
           </tr>
           </tr>
           <tr>
           <tr>
             <th>npm</th>
             <th>npm</th>
-            <td>{ adminHomeContainer.state.npmVersion }</td>
+            <td>{ npmVersion }</td>
           </tr>
           </tr>
           <tr>
           <tr>
             <th>yarn</th>
             <th>yarn</th>
-            <td>{ adminHomeContainer.state.yarnVersion }</td>
+            <td>{ yarnVersion }</td>
           </tr>
           </tr>
         </tbody>
         </tbody>
       </table>
       </table>

+ 20 - 3
packages/app/src/components/DescendantsPageList.tsx

@@ -7,10 +7,12 @@ import {
   IPageInfoForOperation,
   IPageInfoForOperation,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
 import { IPagingResult } from '~/interfaces/paging-result';
 import { IPagingResult } from '~/interfaces/paging-result';
-import { OnDeletedFunction } from '~/interfaces/ui';
+import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import { useIsGuestUser, useIsSharedUser, useIsTrashPage } from '~/stores/context';
 import { useIsGuestUser, useIsSharedUser, useIsTrashPage } from '~/stores/context';
 
 
-import { useSWRxDescendantsPageListForCurrrentPath, useSWRxPageInfoForList, useSWRxPageList } from '~/stores/page';
+import {
+  useSWRxDescendantsPageListForCurrrentPath, useSWRxPageInfoForList, useSWRxPageList, useDescendantsPageListForCurrentPathTermManager,
+} from '~/stores/page';
 import { usePageTreeTermManager } from '~/stores/page-listing';
 import { usePageTreeTermManager } from '~/stores/page-listing';
 import { ForceHideMenuItems, MenuItemType } from './Common/Dropdown/PageItemControl';
 import { ForceHideMenuItems, MenuItemType } from './Common/Dropdown/PageItemControl';
 
 
@@ -24,6 +26,7 @@ type SubstanceProps = {
   setActivePage: (activePage: number) => void,
   setActivePage: (activePage: number) => void,
   forceHideMenuItems?: ForceHideMenuItems,
   forceHideMenuItems?: ForceHideMenuItems,
   onPagesDeleted?: OnDeletedFunction,
   onPagesDeleted?: OnDeletedFunction,
+  onPagePutBacked?: OnPutBackedFunction,
 }
 }
 
 
 const convertToIDataWithMeta = (page: IPageHasId): IDataWithMeta<IPageHasId> => {
 const convertToIDataWithMeta = (page: IPageHasId): IDataWithMeta<IPageHasId> => {
@@ -35,7 +38,7 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const {
   const {
-    pagingResult, activePage, setActivePage, forceHideMenuItems, onPagesDeleted,
+    pagingResult, activePage, setActivePage, forceHideMenuItems, onPagesDeleted, onPagePutBacked,
   } = props;
   } = props;
 
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
@@ -47,6 +50,7 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
 
 
   // for mutation
   // for mutation
   const { advance: advancePt } = usePageTreeTermManager();
   const { advance: advancePt } = usePageTreeTermManager();
+  const { advance: advanceDpl } = useDescendantsPageListForCurrentPathTermManager();
 
 
   // initial data
   // initial data
   if (pagingResult != null) {
   if (pagingResult != null) {
@@ -66,6 +70,17 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
     }
     }
   }, [advancePt, onPagesDeleted, t]);
   }, [advancePt, onPagesDeleted, t]);
 
 
+  const pagePutBackedHandler: OnPutBackedFunction = useCallback((path) => {
+    toastSuccess(t('page_has_been_reverted', { path }));
+
+    advancePt();
+    advanceDpl();
+
+    if (onPagePutBacked != null) {
+      onPagePutBacked(path);
+    }
+  }, [advanceDpl, advancePt, onPagePutBacked, t]);
+
   function setPageNumber(selectedPageNumber) {
   function setPageNumber(selectedPageNumber) {
     setActivePage(selectedPageNumber);
     setActivePage(selectedPageNumber);
   }
   }
@@ -89,6 +104,7 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
         isEnableActions={!isGuestUser}
         isEnableActions={!isGuestUser}
         forceHideMenuItems={forceHideMenuItems}
         forceHideMenuItems={forceHideMenuItems}
         onPagesDeleted={pageDeletedHandler}
         onPagesDeleted={pageDeletedHandler}
+        onPagePutBacked={pagePutBackedHandler}
       />
       />
 
 
       { showPager && (
       { showPager && (
@@ -133,6 +149,7 @@ export const DescendantsPageList = (props: Props): JSX.Element => {
       activePage={activePage}
       activePage={activePage}
       setActivePage={setActivePage}
       setActivePage={setActivePage}
       onPagesDeleted={() => mutate()}
       onPagesDeleted={() => mutate()}
+      onPagePutBacked={() => mutate()}
     />
     />
   );
   );
 };
 };

+ 1 - 0
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -192,6 +192,7 @@ const RevisionRenderer = (props) => {
 RevisionRenderer.propTypes = {
 RevisionRenderer.propTypes = {
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   markdown: PropTypes.string.isRequired,
   markdown: PropTypes.string.isRequired,
+  isRenderable: PropTypes.bool,
   highlightKeywords: PropTypes.arrayOf(PropTypes.string),
   highlightKeywords: PropTypes.arrayOf(PropTypes.string),
   additionalClassName: PropTypes.string,
   additionalClassName: PropTypes.string,
 };
 };

+ 4 - 2
packages/app/src/components/PageList/PageList.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import { IPageWithMeta } from '~/interfaces/page';
 import { IPageWithMeta } from '~/interfaces/page';
-import { OnDeletedFunction } from '~/interfaces/ui';
+import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
 
 import { PageListItemL } from './PageListItemL';
 import { PageListItemL } from './PageListItemL';
@@ -13,12 +13,13 @@ type Props = {
   isEnableActions?: boolean,
   isEnableActions?: boolean,
   forceHideMenuItems?: ForceHideMenuItems,
   forceHideMenuItems?: ForceHideMenuItems,
   onPagesDeleted?: OnDeletedFunction,
   onPagesDeleted?: OnDeletedFunction,
+  onPagePutBacked?: OnPutBackedFunction,
 }
 }
 
 
 const PageList = (props: Props): JSX.Element => {
 const PageList = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const {
   const {
-    pages, isEnableActions, forceHideMenuItems, onPagesDeleted,
+    pages, isEnableActions, forceHideMenuItems, onPagesDeleted, onPagePutBacked,
   } = props;
   } = props;
 
 
   if (pages == null) {
   if (pages == null) {
@@ -38,6 +39,7 @@ const PageList = (props: Props): JSX.Element => {
       isEnableActions={isEnableActions}
       isEnableActions={isEnableActions}
       forceHideMenuItems={forceHideMenuItems}
       forceHideMenuItems={forceHideMenuItems}
       onPageDeleted={onPagesDeleted}
       onPageDeleted={onPagesDeleted}
+      onPagePutBacked={onPagePutBacked}
     />
     />
   ));
   ));
 
 

+ 7 - 4
packages/app/src/components/PageList/PageListItemL.tsx

@@ -23,7 +23,9 @@ import {
   IPageInfoAll, IPageInfoForEntity, IPageInfoForListing, IPageWithMeta, isIPageInfoForListing,
   IPageInfoAll, IPageInfoForEntity, IPageInfoForListing, IPageWithMeta, isIPageInfoForListing,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
 import { IPageSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
 import { IPageSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
-import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
+import {
+  OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
+} from '~/interfaces/ui';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 
 
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
@@ -40,6 +42,7 @@ type Props = {
   onPageDuplicated?: OnDuplicatedFunction,
   onPageDuplicated?: OnDuplicatedFunction,
   onPageRenamed?: OnRenamedFunction,
   onPageRenamed?: OnRenamedFunction,
   onPageDeleted?: OnDeletedFunction,
   onPageDeleted?: OnDeletedFunction,
+  onPagePutBacked?: OnPutBackedFunction,
 }
 }
 
 
 const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (props: Props, ref): JSX.Element => {
 const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (props: Props, ref): JSX.Element => {
@@ -48,7 +51,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     page: { data: pageData, meta: pageMeta }, isSelected, isEnableActions,
     page: { data: pageData, meta: pageMeta }, isSelected, isEnableActions,
     forceHideMenuItems,
     forceHideMenuItems,
     showPageUpdatedTime,
     showPageUpdatedTime,
-    onClickItem, onCheckboxChanged, onPageDuplicated, onPageRenamed, onPageDeleted,
+    onClickItem, onCheckboxChanged, onPageDuplicated, onPageRenamed, onPageDeleted, onPagePutBacked,
   } = props;
   } = props;
 
 
   const inputRef = useRef<HTMLInputElement>(null);
   const inputRef = useRef<HTMLInputElement>(null);
@@ -125,8 +128,8 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
 
 
   const revertMenuItemClickHandler = useCallback(() => {
   const revertMenuItemClickHandler = useCallback(() => {
     const { _id: pageId, path } = pageData;
     const { _id: pageId, path } = pageData;
-    openPutBackPageModal({ pageId, path });
-  }, [openPutBackPageModal, pageData]);
+    openPutBackPageModal({ pageId, path }, { onPutBacked: onPagePutBacked });
+  }, [onPagePutBacked, openPutBackPageModal, pageData]);
 
 
   const styleListGroupItem = (!isDeviceSmallerThanLg && onClickItem != null) ? 'list-group-item-action' : '';
   const styleListGroupItem = (!isDeviceSmallerThanLg && onClickItem != null) ? 'list-group-item-action' : '';
   // background color of list item changes when class "active" exists under 'list-group-item'
   // background color of list item changes when class "active" exists under 'list-group-item'

+ 1 - 2
packages/app/src/components/PutbackPageModal.jsx

@@ -40,9 +40,8 @@ const PutBackPageModal = () => {
         recursively,
         recursively,
       });
       });
 
 
-      const putbackPagePath = encodeURI(response.page.path);
       if (onPutBacked != null) {
       if (onPutBacked != null) {
-        onPutBacked(putbackPagePath);
+        onPutBacked(response.page.path);
       }
       }
       closePutBackPageModal();
       closePutBackPageModal();
     }
     }

+ 25 - 14
packages/app/src/components/Sidebar/CustomSidebar.tsx

@@ -31,9 +31,9 @@ const CustomSidebar: FC<Props> = (props: Props) => {
 
 
   const renderer = appContainer.getRenderer('sidebar');
   const renderer = appContainer.getRenderer('sidebar');
 
 
-  const { data: page, mutate } = useSWRxPageByPath('/Sidebar');
+  const { data: page, error, mutate } = useSWRxPageByPath('/Sidebar');
 
 
-  const isLoading = page === undefined;
+  const isLoading = page === undefined && error == null;
   const markdown = (page?.revision as IRevision | undefined)?.body;
   const markdown = (page?.revision as IRevision | undefined)?.body;
 
 
   return (
   return (
@@ -47,20 +47,31 @@ const CustomSidebar: FC<Props> = (props: Props) => {
           <i className="icon icon-reload"></i>
           <i className="icon icon-reload"></i>
         </button>
         </button>
       </div>
       </div>
-      { !isLoading && markdown == null && <SidebarNotFound /> }
-      {/* eslint-disable-next-line react/no-danger */}
-      { markdown != null && (
-        <div className="p-3">
-          <RevisionRenderer
-            growiRenderer={renderer}
-            markdown={markdown}
-            additionalClassName="grw-custom-sidebar-content"
-          />
-        </div>
-      ) }
+
+      {
+        isLoading && (
+          <div className="text-muted text-center">
+            <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+          </div>
+        )
+      }
+
+      {
+        !isLoading && markdown != null ? (
+          <div className="p-3">
+            <RevisionRenderer
+              growiRenderer={renderer}
+              markdown={markdown}
+              additionalClassName="grw-custom-sidebar-content"
+              isRenderable
+            />
+          </div>
+        ) : (
+          <SidebarNotFound />
+        )
+      }
     </>
     </>
   );
   );
-
 };
 };
 
 
 /**
 /**

+ 41 - 56
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -25,7 +25,7 @@ import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
 import { ItemNode } from './ItemNode';
 import { ItemNode } from './ItemNode';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import {
 import {
-  IPageHasId, IPageInfoAll, IPageToDeleteWithMeta, IPageToRenameWithMeta,
+  IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
 
 
 
 
@@ -39,8 +39,8 @@ interface ItemProps {
   isScrolled: boolean,
   isScrolled: boolean,
   isOpen?: boolean
   isOpen?: boolean
   isEnabledAttachTitleHeader?: boolean
   isEnabledAttachTitleHeader?: boolean
+  onRenamed?(): void
   onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
   onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
-  onClickRenameMenuItem?(pageToRename: IPageToRenameWithMeta): void
   onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
   onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
 }
 }
 
 
@@ -113,7 +113,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const {
   const {
     itemNode, targetPathOrId, isOpen: _isOpen = false, isEnabledAttachTitleHeader,
     itemNode, targetPathOrId, isOpen: _isOpen = false, isEnabledAttachTitleHeader,
-    onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, isEnableActions,
+    onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions,
   } = props;
   } = props;
 
 
   const { page, children } = itemNode;
   const { page, children } = itemNode;
@@ -122,7 +122,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const [isOpen, setIsOpen] = useState(_isOpen);
   const [isOpen, setIsOpen] = useState(_isOpen);
   const [isNewPageInputShown, setNewPageInputShown] = useState(false);
   const [isNewPageInputShown, setNewPageInputShown] = useState(false);
   const [shouldHide, setShouldHide] = useState(false);
   const [shouldHide, setShouldHide] = useState(false);
-  // const [isRenameInputShown, setRenameInputShown] = useState(false);
+  const [isRenameInputShown, setRenameInputShown] = useState(false);
 
 
   const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
   const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
 
 
@@ -258,52 +258,38 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     onClickDuplicateMenuItem(pageToDuplicate);
     onClickDuplicateMenuItem(pageToDuplicate);
   }, [onClickDuplicateMenuItem, page]);
   }, [onClickDuplicateMenuItem, page]);
 
 
+  const renameMenuItemClickHandler = useCallback(() => {
+    setRenameInputShown(true);
+  }, []);
 
 
-  /*
-  * Rename: TODO: rename page title on input form by #87757
-  */
-
-  // const onClickRenameButton = useCallback(async(_pageId: string): Promise<void> => {
-  //   setRenameInputShown(true);
-  // }, []);
-
-  // const onPressEnterForRenameHandler = async(inputText: string) => {
-  //   const parentPath = getParentPagePath(page.path as string)
-  //   const newPagePath = `${parentPath}/${inputText}`;
-
-  //   try {
-  //     setPageTitle(inputText);
-  //     setRenameInputShown(false);
-  //     await apiv3Put('/pages/rename', { newPagePath, pageId: page._id, revisionId: page.revision });
-  //   }
-  //   catch (err) {
-  //     // open ClosableInput and set pageTitle back to the previous title
-  //     setPageTitle(nodePath.basename(pageTitle as string));
-  //     setRenameInputShown(true);
-  //     toastError(err);
-  //   }
-  // };
-
-  const renameMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
-    if (onClickRenameMenuItem == null) {
+  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;
       return;
     }
     }
 
 
-    if (page._id == null || page.revision == null || page.path == null) {
-      throw Error('Any of _id, revision, and path must not be null.');
-    }
+    try {
+      setRenameInputShown(false);
+      await apiv3Put('/pages/rename', {
+        pageId: page._id,
+        revisionId: page.revision,
+        newPagePath,
+      });
 
 
-    const pageToRename: IPageToRenameWithMeta = {
-      data: {
-        _id: page._id,
-        revision: page.revision as string,
-        path: page.path,
-      },
-      meta: pageInfo,
-    };
+      if (onRenamed != null) {
+        onRenamed();
+      }
 
 
-    onClickRenameMenuItem(pageToRename);
-  }, [onClickRenameMenuItem, page]);
+      toastSuccess(t('renamed_pages', { path: page.path }));
+    }
+    catch (err) {
+      setRenameInputShown(true);
+      toastError(err);
+    }
+  };
 
 
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
     if (onClickDeleteMenuItem == null) {
     if (onClickDeleteMenuItem == null) {
@@ -418,7 +404,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     >
     >
       <li
       <li
         ref={(c) => { drag(c); drop(c) }}
         ref={(c) => { drag(c); drop(c) }}
-        className={`list-group-item list-group-item-action border-0 py-0 d-flex align-items-center ${page.isTarget ? 'grw-pagetree-is-target' : ''}`}
+        className={`list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center ${page.isTarget ? 'grw-pagetree-is-target' : ''}`}
         id={page.isTarget ? 'grw-pagetree-is-target' : `grw-pagetree-list-${page._id}`}
         id={page.isTarget ? 'grw-pagetree-is-target' : `grw-pagetree-list-${page._id}`}
       >
       >
         <div className="grw-triangle-container d-flex justify-content-center">
         <div className="grw-triangle-container d-flex justify-content-center">
@@ -434,22 +420,21 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             </button>
             </button>
           )}
           )}
         </div>
         </div>
-        {/* TODO: rename page title on input form by 87757 */}
-        {/* { isRenameInputShown && (
+        { isRenameInputShown && (
           <ClosableTextInput
           <ClosableTextInput
             isShown
             isShown
-            value={nodePath.basename(pageTitle as string)}
+            value={nodePath.basename(page.path ?? '')}
             placeholder={t('Input page name')}
             placeholder={t('Input page name')}
             onClickOutside={() => { setRenameInputShown(false) }}
             onClickOutside={() => { setRenameInputShown(false) }}
             onPressEnter={onPressEnterForRenameHandler}
             onPressEnter={onPressEnterForRenameHandler}
             inputValidator={inputValidator}
             inputValidator={inputValidator}
           />
           />
         )}
         )}
-        { !isRenameInputShown && ( */}
-        <a href={`/${page._id}`} className="grw-pagetree-title-anchor flex-grow-1">
-          <p className={`text-truncate m-auto ${page.isEmpty && 'text-muted'}`}>{nodePath.basename(page.path ?? '') || '/'}</p>
-        </a>
-        {/* )} */}
+        { !isRenameInputShown && (
+          <a href={`/${page._id}`} className="grw-pagetree-title-anchor flex-grow-1">
+            <p className={`text-truncate m-auto ${page.isEmpty && 'text-muted'}`}>{nodePath.basename(page.path ?? '') || '/'}</p>
+          </a>
+        )}
         {(descendantCount > 0) && (
         {(descendantCount > 0) && (
           <div className="grw-pagetree-count-wrapper">
           <div className="grw-pagetree-count-wrapper">
             <ItemCount descendantCount={descendantCount} />
             <ItemCount descendantCount={descendantCount} />
@@ -465,7 +450,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             onClickDeleteMenuItem={deleteMenuItemClickHandler}
             onClickDeleteMenuItem={deleteMenuItemClickHandler}
           >
           >
             {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
             {/* 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">
+            <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
               <i className="icon-options fa fa-rotate-90 text-muted p-1"></i>
               <i className="icon-options fa fa-rotate-90 text-muted p-1"></i>
             </DropdownToggle>
             </DropdownToggle>
           </PageItemControl>
           </PageItemControl>
@@ -474,7 +459,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
             className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
             onClick={onClickPlusButton}
             onClick={onClickPlusButton}
           >
           >
-            <i className="icon-plus text-muted d-block p-1" />
+            <i className="icon-plus text-muted d-block p-0" />
           </button>
           </button>
         </div>
         </div>
       </li>
       </li>
@@ -498,8 +483,8 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
               isScrolled={props.isScrolled}
               isScrolled={props.isScrolled}
               targetPathOrId={targetPathOrId}
               targetPathOrId={targetPathOrId}
               isEnabledAttachTitleHeader={isEnabledAttachTitleHeader}
               isEnabledAttachTitleHeader={isEnabledAttachTitleHeader}
+              onRenamed={onRenamed}
               onClickDuplicateMenuItem={onClickDuplicateMenuItem}
               onClickDuplicateMenuItem={onClickDuplicateMenuItem}
-              onClickRenameMenuItem={onClickRenameMenuItem}
               onClickDeleteMenuItem={onClickDeleteMenuItem}
               onClickDeleteMenuItem={onClickDeleteMenuItem}
             />
             />
           </div>
           </div>

+ 16 - 21
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -3,12 +3,12 @@ import { useTranslation } from 'react-i18next';
 
 
 import { usePageTreeTermManager, useSWRxPageAncestorsChildren, useSWRxRootPage } from '~/stores/page-listing';
 import { usePageTreeTermManager, useSWRxPageAncestorsChildren, useSWRxRootPage } from '~/stores/page-listing';
 import { TargetAndAncestors } from '~/interfaces/page-listing-results';
 import { TargetAndAncestors } from '~/interfaces/page-listing-results';
-import { IPageHasId, IPageToDeleteWithMeta, IPageToRenameWithMeta } from '~/interfaces/page';
-import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
+import { IPageHasId, IPageToDeleteWithMeta } from '~/interfaces/page';
+import { OnDuplicatedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { SocketEventName, UpdateDescCountData, UpdateDescCountRawData } from '~/interfaces/websocket';
 import { SocketEventName, UpdateDescCountData, UpdateDescCountRawData } from '~/interfaces/websocket';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import {
 import {
-  IPageForPageDuplicateModal, usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
+  IPageForPageDuplicateModal, usePageDuplicateModal, usePageDeleteModal,
 } from '~/stores/modal';
 } from '~/stores/modal';
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 
 
@@ -74,8 +74,8 @@ const renderByInitialNode = (
     isScrolled: boolean,
     isScrolled: boolean,
     targetPathOrId?: string,
     targetPathOrId?: string,
     isEnabledAttachTitleHeader?: boolean,
     isEnabledAttachTitleHeader?: boolean,
+    onRenamed?: () => void,
     onClickDuplicateMenuItem?: (pageToDuplicate: IPageForPageDuplicateModal) => void,
     onClickDuplicateMenuItem?: (pageToDuplicate: IPageForPageDuplicateModal) => void,
-    onClickRenameMenuItem?: (pageToRename: IPageToRenameWithMeta) => void,
     onClickDeleteMenuItem?: (pageToDelete: IPageToDeleteWithMeta) => void,
     onClickDeleteMenuItem?: (pageToDelete: IPageToDeleteWithMeta) => void,
 ): JSX.Element => {
 ): JSX.Element => {
 
 
@@ -88,8 +88,8 @@ const renderByInitialNode = (
         isOpen
         isOpen
         isEnabledAttachTitleHeader={isEnabledAttachTitleHeader}
         isEnabledAttachTitleHeader={isEnabledAttachTitleHeader}
         isEnableActions={isEnableActions}
         isEnableActions={isEnableActions}
+        onRenamed={onRenamed}
         onClickDuplicateMenuItem={onClickDuplicateMenuItem}
         onClickDuplicateMenuItem={onClickDuplicateMenuItem}
-        onClickRenameMenuItem={onClickRenameMenuItem}
         onClickDeleteMenuItem={onClickDeleteMenuItem}
         onClickDeleteMenuItem={onClickDeleteMenuItem}
         isScrolled={isScrolled}
         isScrolled={isScrolled}
       />
       />
@@ -125,7 +125,6 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
   const { data: rootPageData, error: error2 } = useSWRxRootPage();
   const { data: rootPageData, error: error2 } = useSWRxRootPage();
   const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
   const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDuplicateModal } = usePageDuplicateModal();
-  const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const [isScrolled, setIsScrolled] = useState(false);
   const [isScrolled, setIsScrolled] = useState(false);
 
 
@@ -155,15 +154,23 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
       return;
       return;
     }
     }
 
 
-    // socket
     socket.on(SocketEventName.UpdateDescCount, (data: UpdateDescCountRawData) => {
     socket.on(SocketEventName.UpdateDescCount, (data: UpdateDescCountRawData) => {
       // save to global state
       // save to global state
       const newData: UpdateDescCountData = new Map(Object.entries(data));
       const newData: UpdateDescCountData = new Map(Object.entries(data));
 
 
       updatePtDescCountMap(newData);
       updatePtDescCountMap(newData);
     });
     });
+
+    return () => { socket.off(SocketEventName.UpdateDescCount) };
+
   }, [socket, ptDescCountMap, updatePtDescCountMap]);
   }, [socket, ptDescCountMap, updatePtDescCountMap]);
 
 
+  const onRenamed = () => {
+    advancePt();
+    advanceFts();
+    advanceDpl();
+  };
+
   const onClickDuplicateMenuItem = (pageToDuplicate: IPageForPageDuplicateModal) => {
   const onClickDuplicateMenuItem = (pageToDuplicate: IPageForPageDuplicateModal) => {
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
@@ -177,18 +184,6 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
     openDuplicateModal(pageToDuplicate, { onDuplicated: duplicatedHandler });
     openDuplicateModal(pageToDuplicate, { onDuplicated: duplicatedHandler });
   };
   };
 
 
-  const onClickRenameMenuItem = (pageToRename: IPageToRenameWithMeta) => {
-    const renamedHandler: OnRenamedFunction = (path) => {
-      toastSuccess(t('renamed_pages', { path }));
-
-      advancePt();
-      advanceFts();
-      advanceDpl();
-    };
-
-    openRenameModal(pageToRename, { onRenamed: renamedHandler });
-  };
-
   const onClickDeleteMenuItem = (pageToDelete: IPageToDeleteWithMeta) => {
   const onClickDeleteMenuItem = (pageToDelete: IPageToDeleteWithMeta) => {
     const onDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
     const onDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
       if (typeof pathOrPathsToDelete !== 'string') {
       if (typeof pathOrPathsToDelete !== 'string') {
@@ -225,7 +220,7 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
     const initialNode = generateInitialNodeAfterResponse(ancestorsChildrenData.ancestorsChildren, new ItemNode(rootPageData.rootPage));
     const initialNode = generateInitialNodeAfterResponse(ancestorsChildrenData.ancestorsChildren, new ItemNode(rootPageData.rootPage));
     return renderByInitialNode(
     return renderByInitialNode(
       // eslint-disable-next-line max-len
       // eslint-disable-next-line max-len
-      initialNode, isEnableActions, isScrolled, targetPathOrId, isEnabledAttachTitleHeader, onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem,
+      initialNode, isEnableActions, isScrolled, targetPathOrId, isEnabledAttachTitleHeader, onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem,
     );
     );
   }
   }
 
 
@@ -236,7 +231,7 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
     const initialNode = generateInitialNodeBeforeResponse(targetAndAncestorsData.targetAndAncestors);
     const initialNode = generateInitialNodeBeforeResponse(targetAndAncestorsData.targetAndAncestors);
     return renderByInitialNode(
     return renderByInitialNode(
       // eslint-disable-next-line max-len
       // eslint-disable-next-line max-len
-      initialNode, isEnableActions, isScrolled, targetPathOrId, isEnabledAttachTitleHeader, onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem,
+      initialNode, isEnableActions, isScrolled, targetPathOrId, isEnabledAttachTitleHeader, onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem,
     );
     );
   }
   }
 
 

+ 6 - 1
packages/app/src/server/routes/apiv3/personal-setting.js

@@ -76,7 +76,12 @@ module.exports = (crowi) => {
   const validator = {
   const validator = {
     personal: [
     personal: [
       body('name').isString().not().isEmpty(),
       body('name').isString().not().isEmpty(),
-      body('email').isEmail(),
+      body('email')
+        .isEmail()
+        .custom((email) => {
+          if (!User.isEmailValid(email)) throw new Error('email is not included in whitelist');
+          return true;
+        }),
       body('lang').isString().isIn(listLocaleIds()),
       body('lang').isString().isIn(listLocaleIds()),
       body('isEmailPublished').isBoolean(),
       body('isEmailPublished').isBoolean(),
     ],
     ],

+ 1 - 1
packages/app/src/server/routes/index.js

@@ -203,7 +203,7 @@ module.exports = function(crowi, app) {
     .use(forgotPassword.handleErrosMiddleware));
     .use(forgotPassword.handleErrosMiddleware));
 
 
   app.use('/_private-legacy-pages', express.Router()
   app.use('/_private-legacy-pages', express.Router()
-    .get('/', privateLegacyPages.renderPrivateLegacyPages));
+    .get('/', injectUserUISettings, privateLegacyPages.renderPrivateLegacyPages));
   app.use('/user-activation', express.Router()
   app.use('/user-activation', express.Router()
     .get('/:token', apiLimiter, applicationInstalled, injectUserRegistrationOrderByTokenMiddleware, userActivation.form)
     .get('/:token', apiLimiter, applicationInstalled, injectUserRegistrationOrderByTokenMiddleware, userActivation.form)
     .use(userActivation.tokenErrorHandlerMiddeware));
     .use(userActivation.tokenErrorHandlerMiddeware));

+ 6 - 0
packages/app/src/styles/_page-tree.scss

@@ -5,6 +5,12 @@ $grw-pagetree-item-padding-left: 10px;
 .grw-pagetree {
 .grw-pagetree {
   min-height: calc(100vh - ($grw-navbar-height + $grw-navbar-border-width + $grw-sidebar-content-header-height + $grw-sidebar-content-footer-height));
   min-height: calc(100vh - ($grw-navbar-height + $grw-navbar-border-width + $grw-sidebar-content-header-height + $grw-sidebar-content-footer-height));
 
 
+  .btn-page-item-control {
+    .icon-plus::before {
+      font-size: 18px;
+    }
+  }
+
   .list-group-item {
   .list-group-item {
     .grw-visible-on-hover {
     .grw-visible-on-hover {
       display: none;
       display: none;

+ 2 - 0
packages/app/test/cypress/integration/2-basic-features/access-to-admin-page.spec.ts

@@ -37,6 +37,8 @@ context('Access to Admin page', () => {
   it('/admin is successfully loaded', () => {
   it('/admin is successfully loaded', () => {
     cy.visit('/admin');
     cy.visit('/admin');
     cy.getByTestid('admin-home').should('be.visible');
     cy.getByTestid('admin-home').should('be.visible');
+    cy.getByTestid('admin-system-information-table').should('be.visible');
+    cy.getByTestid('admin-installed-plugin-table').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin`);
     cy.screenshot(`${ssPrefix}-admin`);
   });
   });
 
 

+ 0 - 2
packages/app/test/cypress/integration/2-basic-features/access-to-page.spec.ts

@@ -29,8 +29,6 @@ context('Access to page', () => {
 
 
   it('/Sandbox with anchor hash is successfully loaded', () => {
   it('/Sandbox with anchor hash is successfully loaded', () => {
     cy.visit('/Sandbox#Headers');
     cy.visit('/Sandbox#Headers');
-    cy.getByTestid('grw-fab-create-page').should('have.class', 'fadeInUp').should('be.visible');
-    cy.getByTestid('grw-fab-return-to-top').should('have.class', 'fadeInUp').should('be.visible');
     cy.screenshot(`${ssPrefix}-sandbox-headers`, {
     cy.screenshot(`${ssPrefix}-sandbox-headers`, {
       disableTimersAndAnimations: false,
       disableTimersAndAnimations: false,
     });
     });

+ 10 - 10
packages/app/test/cypress/integration/2-basic-features/open-presentation-modal.spec.ts

@@ -1,5 +1,5 @@
-
 context('Open presentation modal', () => {
 context('Open presentation modal', () => {
+
   const ssPrefix = 'access-to-presentation-modal-';
   const ssPrefix = 'access-to-presentation-modal-';
 
 
   let connectSid: string | undefined;
   let connectSid: string | undefined;
@@ -20,12 +20,12 @@ context('Open presentation modal', () => {
     }
     }
   });
   });
 
 
-  it('PageCreateModal for "/" is shown successfully', () => {
+  it('PresentationModal for "/" is shown successfully', () => {
     cy.visit('/');
     cy.visit('/');
 
 
     cy.get('#grw-subnav-container').within(() => {
     cy.get('#grw-subnav-container').within(() => {
-      cy.getByTestid('open-page-item-control-btn').click({force: true})
-      cy.getByTestid('open-presentation-modal-btn').click({force: true})
+      cy.getByTestid('open-page-item-control-btn').click({force: true});
+      cy.getByTestid('open-presentation-modal-btn').click({force: true});
     });
     });
 
 
     // eslint-disable-next-line cypress/no-unnecessary-waiting
     // eslint-disable-next-line cypress/no-unnecessary-waiting
@@ -33,12 +33,12 @@ context('Open presentation modal', () => {
     cy.screenshot(`${ssPrefix}-opne-top`);
     cy.screenshot(`${ssPrefix}-opne-top`);
   });
   });
 
 
-  it('PageCreateModal for "/Sandbox/Bootstrap4" is shown successfully', () => {
+  it('PresentationModal for "/Sandbox/Bootstrap4" is shown successfully', () => {
     cy.visit('/Sandbox/Bootstrap4');
     cy.visit('/Sandbox/Bootstrap4');
 
 
     cy.get('#grw-subnav-container').within(() => {
     cy.get('#grw-subnav-container').within(() => {
-      cy.getByTestid('open-page-item-control-btn').click({force: true})
-      cy.getByTestid('open-presentation-modal-btn').click({force: true})
+      cy.getByTestid('open-page-item-control-btn').click({force: true});
+      cy.getByTestid('open-presentation-modal-btn').click({force: true});
     });
     });
 
 
     // eslint-disable-next-line cypress/no-unnecessary-waiting
     // eslint-disable-next-line cypress/no-unnecessary-waiting
@@ -46,12 +46,12 @@ context('Open presentation modal', () => {
     cy.screenshot(`${ssPrefix}-open-bootstrap4`);
     cy.screenshot(`${ssPrefix}-open-bootstrap4`);
   });
   });
 
 
-  it('PageCreateModal for /Sandbox/Bootstrap4#Cards" is shown successfully', () => {
+  it('PresentationModal for /Sandbox/Bootstrap4#Cards" is shown successfully', () => {
     cy.visit('/Sandbox/Bootstrap4#Cards');
     cy.visit('/Sandbox/Bootstrap4#Cards');
 
 
     cy.get('#grw-subnav-container').within(() => {
     cy.get('#grw-subnav-container').within(() => {
-      cy.getByTestid('open-page-item-control-btn').click({force: true})
-      cy.getByTestid('open-presentation-modal-btn').click({force: true})
+      cy.getByTestid('open-page-item-control-btn').click({force: true});
+      cy.getByTestid('open-presentation-modal-btn').click({force: true});
     });
     });
 
 
     // eslint-disable-next-line cypress/no-unnecessary-waiting
     // eslint-disable-next-line cypress/no-unnecessary-waiting