Browse Source

Merge branch 'feat/80324-adjust-design-for-left-pane' into feat/80324-81761-fix-search-control-position

* feat/80324-adjust-design-for-left-pane: (49 commits)
  81922 add default value
  82200 margin in search control
  81405 remove unused code
  81405 remove unnecessary comments
  81405 get value from crowi configManager
  81405 read pageLimitationContainer from adminCustomizeContainer
  81405 change method name
  81405 use customizeParams for the default liimit number for pages to display
  81403 add tentative selectbox to search control
  refs #80335: Detach page control from page container - Fix import of api v3 client
  refs #80335: Detach page control from page container - Rename param name for rename modal.
  refs #80335: Detach page control from page container - Detach PageDuplicateModal from PageContainer
  refs #80335: Detach page control from page container - Detach BookmarkButton from PageContainer
  refs #80335: Detach page control from page container - Detach PageRenameModal from PageContainer
  refs #80335: Detach page control from page container - small changes
  refs #80335: Detach page control from page container - Detach PutBackPageModal from PageContainer and AppContainer(, which provided by Unstated)
  refs #80335: Detach page control from page container - Retry: Detach PageDeleteModal from PageContainer and AppContainer
  refs #80335: Detach page control from page container - Retry: Detach rename from pageContainer
  refs #80335: Detach page control from page container - Retry: Detach revertRemove from pageContainer
  refs #80335: Detach page control from page container - Retry: Detach deletePage from pageContainer
  ...
Mao 4 years ago
parent
commit
452c7d6f60
27 changed files with 251 additions and 214 deletions
  1. 4 1
      packages/app/resource/locales/en_US/translation.json
  2. 3 1
      packages/app/resource/locales/ja_JP/translation.json
  3. 3 1
      packages/app/resource/locales/zh_CN/translation.json
  4. 0 49
      packages/app/src/client/services/PageContainer.js
  5. 24 12
      packages/app/src/components/BookmarkButton.jsx
  6. 15 1
      packages/app/src/components/Navbar/SubNavButtons.jsx
  7. 9 1
      packages/app/src/components/Page/PageManagement.jsx
  8. 4 1
      packages/app/src/components/Page/TrashPageAlert.jsx
  9. 17 11
      packages/app/src/components/PageDeleteModal.jsx
  10. 7 5
      packages/app/src/components/PageDuplicateModal.jsx
  11. 1 1
      packages/app/src/components/PageList/Page.jsx
  12. 25 22
      packages/app/src/components/PageRenameModal.jsx
  13. 13 12
      packages/app/src/components/PutbackPageModal.jsx
  14. 15 11
      packages/app/src/components/SearchPage.jsx
  15. 1 1
      packages/app/src/components/SearchPage/SearchControl.tsx
  16. 15 3
      packages/app/src/components/SearchPage/SearchPageLayout.tsx
  17. 25 31
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  18. 2 1
      packages/app/src/components/SearchPage/SearchResultListItem.tsx
  19. 1 1
      packages/app/src/components/SearchTypeahead.jsx
  20. 1 1
      packages/app/src/interfaces/search.ts
  21. 1 0
      packages/app/src/server/models/config.ts
  22. 1 2
      packages/app/src/server/service/search.js
  23. 5 2
      packages/app/src/styles/_search.scss
  24. 2 2
      packages/core/src/models/devided-page-path.js
  25. 1 1
      packages/plugin-lsx/src/client/js/components/LsxPageList/PagePathWrapper.jsx
  26. 0 40
      packages/ui/src/components/PagePath/PagePathLabel.jsx
  27. 56 0
      packages/ui/src/components/PagePath/PagePathLabel.tsx

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

@@ -576,7 +576,10 @@
     "deletion_modal_header": "Delete page",
     "deletion_modal_header": "Delete page",
     "delete_completely": "Delete completely",
     "delete_completely": "Delete completely",
     "include_certain_path" : "Include {{pathToInclude}} path ",
     "include_certain_path" : "Include {{pathToInclude}} path ",
-    "delete_all_selected_page" : "Delete All"
+    "delete_all_selected_page" : "Delete All",
+    "number_of_list_to_display" : "Display",
+    "page_number_unit" : "pages"
+
   },
   },
   "security_setting": {
   "security_setting": {
     "Guest Users Access": "Guest users access",
     "Guest Users Access": "Guest users access",

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

@@ -576,7 +576,9 @@
     "deletion_modal_header": "以下のページを削除",
     "deletion_modal_header": "以下のページを削除",
     "delete_completely": "完全に削除する",
     "delete_completely": "完全に削除する",
     "include_certain_path": "{{pathToInclude}}下を含む ",
     "include_certain_path": "{{pathToInclude}}下を含む ",
-    "delete_all_selected_page" : "一括削除"
+    "delete_all_selected_page" : "一括削除",
+    "number_of_list_to_display" : "表示件数",
+    "page_number_unit" : "件"
   },
   },
   "security_setting": {
   "security_setting": {
     "Guest Users Access": "ゲストユーザーのアクセス",
     "Guest Users Access": "ゲストユーザーのアクセス",

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

@@ -849,7 +849,9 @@
 		"deletion_modal_header": "删除页",
 		"deletion_modal_header": "删除页",
 		"delete_completely": "完全删除",
 		"delete_completely": "完全删除",
     "include_certain_path": "包含 {{pathToInclude}} 路径 ",
     "include_certain_path": "包含 {{pathToInclude}} 路径 ",
-    "delete_all_selected_page": "删除所有"
+    "delete_all_selected_page": "删除所有",
+    "number_of_list_to_display" : "显示器的数量",
+    "page_number_unit" : "例"
 	},
 	},
 	"to_cloud_settings": "進入 GROWI.cloud 的管理界面",
 	"to_cloud_settings": "進入 GROWI.cloud 的管理界面",
 	"login": {
 	"login": {

+ 0 - 49
packages/app/src/client/services/PageContainer.js

@@ -328,12 +328,6 @@ export default class PageContainer extends Container {
     });
     });
   }
   }
 
 
-  async toggleBookmark() {
-    const bool = !this.state.isBookmarked;
-    await this.appContainer.apiv3Put('/bookmarks', { pageId: this.state.pageId, bool });
-    return this.retrieveBookmarkInfo();
-  }
-
   async checkAndUpdateImageUrlCached(users) {
   async checkAndUpdateImageUrlCached(users) {
     const noImageCacheUsers = users.filter((user) => { return user.imageUrlCached == null });
     const noImageCacheUsers = users.filter((user) => { return user.imageUrlCached == null });
     if (noImageCacheUsers.length === 0) {
     if (noImageCacheUsers.length === 0) {
@@ -529,49 +523,6 @@ export default class PageContainer extends Container {
     return res;
     return res;
   }
   }
 
 
-  deletePage(isRecursively, isCompletely) {
-    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-
-    // control flag
-    const completely = isCompletely ? true : null;
-    const recursively = isRecursively ? true : null;
-
-    return this.appContainer.apiPost('/pages.remove', {
-      recursively,
-      completely,
-      page_id: this.state.pageId,
-      revision_id: this.state.revisionId,
-    });
-
-  }
-
-  revertRemove(isRecursively) {
-    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-
-    // control flag
-    const recursively = isRecursively ? true : null;
-
-    return this.appContainer.apiPost('/pages.revertRemove', {
-      recursively,
-      page_id: this.state.pageId,
-    });
-  }
-
-  rename(newPagePath, isRecursively, isRenameRedirect, isRemainMetadata) {
-    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-    const { pageId, revisionId, path } = this.state;
-
-    return this.appContainer.apiv3Put('/pages/rename', {
-      revisionId,
-      pageId,
-      isRecursively,
-      isRenameRedirect,
-      isRemainMetadata,
-      newPagePath,
-      path,
-    });
-  }
-
   showSuccessToastr() {
   showSuccessToastr() {
     toastr.success(undefined, 'Saved successfully', {
     toastr.success(undefined, 'Saved successfully', {
       closeButton: true,
       closeButton: true,

+ 24 - 12
packages/app/src/components/BookmarkButton.jsx

@@ -6,7 +6,8 @@ import { withTranslation } from 'react-i18next';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
-import PageContainer from '~/client/services/PageContainer';
+import { apiv3Put } from '~/client/util/apiv3-client';
+
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 
 
 class BookmarkButton extends React.Component {
 class BookmarkButton extends React.Component {
@@ -18,7 +19,9 @@ class BookmarkButton extends React.Component {
   }
   }
 
 
   async handleClick() {
   async handleClick() {
-    const { appContainer, pageContainer } = this.props;
+    const {
+      appContainer, pageId, isBookmarked, onChangeInvoked,
+    } = this.props;
     const { isGuestUser } = appContainer;
     const { isGuestUser } = appContainer;
 
 
     if (isGuestUser) {
     if (isGuestUser) {
@@ -26,16 +29,21 @@ class BookmarkButton extends React.Component {
     }
     }
 
 
     try {
     try {
-      pageContainer.toggleBookmark();
+      const bool = !isBookmarked;
+      await apiv3Put('/bookmarks', { pageId, bool });
+      if (onChangeInvoked != null) {
+        onChangeInvoked();
+      }
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
   }
   }
 
 
-
   render() {
   render() {
-    const { appContainer, pageContainer, t } = this.props;
+    const {
+      appContainer, t, isBookmarked, sumOfBookmarks,
+    } = this.props;
     const { isGuestUser } = appContainer;
     const { isGuestUser } = appContainer;
 
 
     return (
     return (
@@ -45,12 +53,14 @@ class BookmarkButton extends React.Component {
           id="bookmark-button"
           id="bookmark-button"
           onClick={this.handleClick}
           onClick={this.handleClick}
           className={`btn btn-bookmark border-0
           className={`btn btn-bookmark border-0
-          ${`btn-${this.props.size}`} ${pageContainer.state.isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
+          ${`btn-${this.props.size}`} ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
         >
         >
           <i className="icon-star mr-3"></i>
           <i className="icon-star mr-3"></i>
-          <span className="total-bookmarks">
-            {pageContainer.state.sumOfBookmarks}
-          </span>
+          {sumOfBookmarks && (
+            <span className="total-bookmarks">
+              {sumOfBookmarks}
+            </span>
+          )}
         </button>
         </button>
 
 
         {isGuestUser && (
         {isGuestUser && (
@@ -67,13 +77,15 @@ class BookmarkButton extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const BookmarkButtonWrapper = withUnstatedContainers(BookmarkButton, [AppContainer, PageContainer]);
+const BookmarkButtonWrapper = withUnstatedContainers(BookmarkButton, [AppContainer]);
 
 
 BookmarkButton.propTypes = {
 BookmarkButton.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
-  pageId: PropTypes.string,
+  pageId: PropTypes.string.isRequired,
+  isBookmarked: PropTypes.bool.isRequired,
+  sumOfBookmarks: PropTypes.number,
+  onChangeInvoked: PropTypes.func,
   t: PropTypes.func.isRequired,
   t: PropTypes.func.isRequired,
   size: PropTypes.string,
   size: PropTypes.string,
 };
 };

+ 15 - 1
packages/app/src/components/Navbar/SubNavButtons.jsx

@@ -4,11 +4,14 @@ import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
+import loggerFactory from '~/utils/logger';
 
 
 import BookmarkButton from '../BookmarkButton';
 import BookmarkButton from '../BookmarkButton';
 import LikeButtons from '../LikeButtons';
 import LikeButtons from '../LikeButtons';
 import PageManagement from '../Page/PageManagement';
 import PageManagement from '../Page/PageManagement';
 
 
+const logger = loggerFactory('growi:SubnavButtons');
+
 const SubnavButtons = (props) => {
 const SubnavButtons = (props) => {
   const {
   const {
     appContainer, navigationContainer, pageContainer, isCompactMode,
     appContainer, navigationContainer, pageContainer, isCompactMode,
@@ -18,6 +21,12 @@ const SubnavButtons = (props) => {
 
 
   /* eslint-disable react/prop-types */
   /* eslint-disable react/prop-types */
   const PageReactionButtons = ({ pageContainer }) => {
   const PageReactionButtons = ({ pageContainer }) => {
+    const { pageId, isBookmarked, sumOfBookmarks } = pageContainer.state;
+
+    const onChangeInvoked = () => {
+      if (pageContainer.retrieveBookmarkInfo == null) { logger.error('retrieveBookmarkInfo is null') }
+      else { pageContainer.retrieveBookmarkInfo() }
+    };
 
 
     return (
     return (
       <>
       <>
@@ -27,7 +36,12 @@ const SubnavButtons = (props) => {
           </span>
           </span>
         )}
         )}
         <span>
         <span>
-          <BookmarkButton />
+          <BookmarkButton
+            pageId={pageId}
+            isBookmarked={isBookmarked}
+            sumOfBookmarks={sumOfBookmarks}
+            onChangeInvoked={onChangeInvoked}
+          />
         </span>
         </span>
       </>
       </>
     );
     );

+ 9 - 1
packages/app/src/components/Page/PageManagement.jsx

@@ -22,7 +22,9 @@ const PageManagement = (props) => {
   const {
   const {
     t, appContainer, pageContainer, isCompactMode,
     t, appContainer, pageContainer, isCompactMode,
   } = props;
   } = props;
-  const { path, isDeletable, isAbleToDeleteCompletely } = pageContainer.state;
+  const {
+    pageId, revisionId, path, isDeletable, isAbleToDeleteCompletely,
+  } = pageContainer.state;
 
 
   const { currentUser } = appContainer;
   const { currentUser } = appContainer;
   const isTopPagePath = isTopPage(path);
   const isTopPagePath = isTopPage(path);
@@ -165,11 +167,15 @@ const PageManagement = (props) => {
         <PageRenameModal
         <PageRenameModal
           isOpen={isPageRenameModalShown}
           isOpen={isPageRenameModalShown}
           onClose={closePageRenameModalHandler}
           onClose={closePageRenameModalHandler}
+          pageId={pageId}
+          revisionId={revisionId}
           path={path}
           path={path}
         />
         />
         <PageDuplicateModal
         <PageDuplicateModal
           isOpen={isPageDuplicateModalShown}
           isOpen={isPageDuplicateModalShown}
           onClose={closePageDuplicateModalHandler}
           onClose={closePageDuplicateModalHandler}
+          pageId={pageId}
+          path={path}
         />
         />
         <CreateTemplateModal
         <CreateTemplateModal
           isOpen={isPageTemplateModalShown}
           isOpen={isPageTemplateModalShown}
@@ -178,6 +184,8 @@ const PageManagement = (props) => {
         <PageDeleteModal
         <PageDeleteModal
           isOpen={isPageDeleteModalShown}
           isOpen={isPageDeleteModalShown}
           onClose={closePageDeleteModalHandler}
           onClose={closePageDeleteModalHandler}
+          pageId={pageId}
+          revisionId={revisionId}
           path={path}
           path={path}
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}
         />
         />

+ 4 - 1
packages/app/src/components/Page/TrashPageAlert.jsx

@@ -15,7 +15,7 @@ import PageDeleteModal from '../PageDeleteModal';
 const TrashPageAlert = (props) => {
 const TrashPageAlert = (props) => {
   const { t, pageContainer } = props;
   const { t, pageContainer } = props;
   const {
   const {
-    path, isDeleted, lastUpdateUsername, updatedAt, deletedUserName, deletedAt, isAbleToDeleteCompletely,
+    pageId, revisionId, path, isDeleted, lastUpdateUsername, updatedAt, deletedUserName, deletedAt, isAbleToDeleteCompletely,
   } = pageContainer.state;
   } = pageContainer.state;
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
   const [isPutbackPageModalShown, setIsPutbackPageModalShown] = useState(false);
   const [isPutbackPageModalShown, setIsPutbackPageModalShown] = useState(false);
@@ -92,11 +92,14 @@ const TrashPageAlert = (props) => {
         <PutbackPageModal
         <PutbackPageModal
           isOpen={isPutbackPageModalShown}
           isOpen={isPutbackPageModalShown}
           onClose={closePutbackPageModalHandler}
           onClose={closePutbackPageModalHandler}
+          pageId={pageId}
           path={path}
           path={path}
         />
         />
         <PageDeleteModal
         <PageDeleteModal
           isOpen={isPageDeleteModalShown}
           isOpen={isPageDeleteModalShown}
           onClose={opclosePageDeleteModalHandler}
           onClose={opclosePageDeleteModalHandler}
+          pageId={pageId}
+          revisionId={revisionId}
           path={path}
           path={path}
           isDeleteCompletelyModal
           isDeleteCompletelyModal
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}

+ 17 - 11
packages/app/src/components/PageDeleteModal.jsx

@@ -7,8 +7,7 @@ import {
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import { withUnstatedContainers } from './UnstatedUtils';
-import PageContainer from '~/client/services/PageContainer';
+import { apiPost } from '~/client/util/apiv1-client';
 
 
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 
@@ -27,7 +26,7 @@ const deleteIconAndKey = {
 
 
 const PageDeleteModal = (props) => {
 const PageDeleteModal = (props) => {
   const {
   const {
-    t, pageContainer, isOpen, onClose, isDeleteCompletelyModal, path, isAbleToDeleteCompletely,
+    t, isOpen, onClose, isDeleteCompletelyModal, pageId, revisionId, path, isAbleToDeleteCompletely,
   } = props;
   } = props;
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
   const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
   const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
@@ -50,7 +49,18 @@ const PageDeleteModal = (props) => {
     setErrs(null);
     setErrs(null);
 
 
     try {
     try {
-      const response = await pageContainer.deletePage(isDeleteRecursively, isDeleteCompletely);
+      // control flag
+      // If is it not true, Request value must be `null`.
+      const recursively = isDeleteRecursively ? true : null;
+      const completely = isDeleteCompletely ? true : null;
+
+      const response = await apiPost('/pages.remove', {
+        page_id: pageId,
+        revision_id: revisionId,
+        recursively,
+        completely,
+      });
+
       const trashPagePath = response.page.path;
       const trashPagePath = response.page.path;
       window.location.href = encodeURI(trashPagePath);
       window.location.href = encodeURI(trashPagePath);
     }
     }
@@ -133,18 +143,14 @@ const PageDeleteModal = (props) => {
   );
   );
 };
 };
 
 
-/**
- * Wrapper component for using unstated
- */
-const PageDeleteModalWrapper = withUnstatedContainers(PageDeleteModal, [PageContainer]);
-
 PageDeleteModal.propTypes = {
 PageDeleteModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
   onClose: PropTypes.func.isRequired,
 
 
+  pageId: PropTypes.string.isRequired,
+  revisionId: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
   isDeleteCompletelyModal: PropTypes.bool,
   isDeleteCompletelyModal: PropTypes.bool,
   isAbleToDeleteCompletely: PropTypes.bool,
   isAbleToDeleteCompletely: PropTypes.bool,
@@ -154,4 +160,4 @@ PageDeleteModal.defaultProps = {
   isDeleteCompletelyModal: false,
   isDeleteCompletelyModal: false,
 };
 };
 
 
-export default withTranslation()(PageDeleteModalWrapper);
+export default withTranslation()(PageDeleteModal);

+ 7 - 5
packages/app/src/components/PageDuplicateModal.jsx

@@ -11,7 +11,6 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
 import PagePathAutoComplete from './PagePathAutoComplete';
 import PagePathAutoComplete from './PagePathAutoComplete';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ComparePathsTable from './ComparePathsTable';
 import ComparePathsTable from './ComparePathsTable';
@@ -20,11 +19,12 @@ import DuplicatePathsTable from './DuplicatedPathsTable';
 const LIMIT_FOR_LIST = 10;
 const LIMIT_FOR_LIST = 10;
 
 
 const PageDuplicateModal = (props) => {
 const PageDuplicateModal = (props) => {
-  const { t, appContainer, pageContainer } = props;
+  const {
+    t, appContainer, pageId, path,
+  } = props;
 
 
   const config = appContainer.getConfig();
   const config = appContainer.getConfig();
   const isReachable = config.isSearchServiceReachable;
   const isReachable = config.isSearchServiceReachable;
-  const { pageId, path } = pageContainer.state;
   const { crowi } = appContainer.config;
   const { crowi } = appContainer.config;
 
 
   const [pageNameInput, setPageNameInput] = useState(path);
   const [pageNameInput, setPageNameInput] = useState(path);
@@ -213,16 +213,18 @@ const PageDuplicateModal = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const PageDuplicateModallWrapper = withUnstatedContainers(PageDuplicateModal, [AppContainer, PageContainer]);
+const PageDuplicateModallWrapper = withUnstatedContainers(PageDuplicateModal, [AppContainer]);
 
 
 
 
 PageDuplicateModal.propTypes = {
 PageDuplicateModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
   onClose: PropTypes.func.isRequired,
+
+  pageId: PropTypes.string.isRequired,
+  path: PropTypes.string.isRequired,
 };
 };
 
 
 export default withTranslation()(PageDuplicateModallWrapper);
 export default withTranslation()(PageDuplicateModallWrapper);

+ 1 - 1
packages/app/src/components/PageList/Page.jsx

@@ -11,7 +11,7 @@ export default class Page extends React.Component {
       page, noLink,
       page, noLink,
     } = this.props;
     } = this.props;
 
 
-    let pagePathElem = <PagePathLabel page={page} additionalClassNames={['mx-1']} />;
+    let pagePathElem = <PagePathLabel path={page.path} additionalClassNames={['mx-1']} />;
     if (!noLink) {
     if (!noLink) {
       pagePathElem = <a className="text-break" href={page.path}>{pagePathElem}</a>;
       pagePathElem = <a className="text-break" href={page.path}>{pagePathElem}</a>;
     }
     }

+ 25 - 22
packages/app/src/components/PageRenameModal.jsx

@@ -14,7 +14,9 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
+
+import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
+
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ComparePathsTable from './ComparePathsTable';
 import ComparePathsTable from './ComparePathsTable';
 import DuplicatedPathsTable from './DuplicatedPathsTable';
 import DuplicatedPathsTable from './DuplicatedPathsTable';
@@ -22,11 +24,9 @@ import DuplicatedPathsTable from './DuplicatedPathsTable';
 
 
 const PageRenameModal = (props) => {
 const PageRenameModal = (props) => {
   const {
   const {
-    t, appContainer, pageContainer,
+    t, appContainer, path, pageId, revisionId,
   } = props;
   } = props;
 
 
-  const { path } = pageContainer.state;
-
   const { crowi } = appContainer.config;
   const { crowi } = appContainer.config;
 
 
   const [pageNameInput, setPageNameInput] = useState(path);
   const [pageNameInput, setPageNameInput] = useState(path);
@@ -37,7 +37,7 @@ const PageRenameModal = (props) => {
   const [existingPaths, setExistingPaths] = useState([]);
   const [existingPaths, setExistingPaths] = useState([]);
   const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
   const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
   const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
   const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
-  const [isRenameMetadata, SetIsRenameMetadata] = useState(false);
+  const [isRemainMetadata, SetIsRemainMetadata] = useState(false);
   const [subordinatedError] = useState(null);
   const [subordinatedError] = useState(null);
   const [isRenameRecursivelyWithoutExistPath, setIsRenameRecursivelyWithoutExistPath] = useState(true);
   const [isRenameRecursivelyWithoutExistPath, setIsRenameRecursivelyWithoutExistPath] = useState(true);
 
 
@@ -53,13 +53,13 @@ const PageRenameModal = (props) => {
     SetIsRenameRedirect(!isRenameRedirect);
     SetIsRenameRedirect(!isRenameRedirect);
   }
   }
 
 
-  function changeIsRenameMetadataHandler() {
-    SetIsRenameMetadata(!isRenameMetadata);
+  function changeIsRemainMetadataHandler() {
+    SetIsRemainMetadata(!isRemainMetadata);
   }
   }
 
 
   const updateSubordinatedList = useCallback(async() => {
   const updateSubordinatedList = useCallback(async() => {
     try {
     try {
-      const res = await appContainer.apiv3Get('/pages/subordinated-list', { path });
+      const res = await apiv3Get('/pages/subordinated-list', { path });
       const { subordinatedPaths } = res.data;
       const { subordinatedPaths } = res.data;
       setSubordinatedPages(subordinatedPaths);
       setSubordinatedPages(subordinatedPaths);
     }
     }
@@ -67,7 +67,7 @@ const PageRenameModal = (props) => {
       setErrs(err);
       setErrs(err);
       toastError(t('modal_rename.label.Fail to get subordinated pages'));
       toastError(t('modal_rename.label.Fail to get subordinated pages'));
     }
     }
-  }, [appContainer, path, t]);
+  }, [path, t]);
 
 
   useEffect(() => {
   useEffect(() => {
     if (props.isOpen) {
     if (props.isOpen) {
@@ -78,7 +78,7 @@ const PageRenameModal = (props) => {
 
 
   const checkExistPaths = async(newParentPath) => {
   const checkExistPaths = async(newParentPath) => {
     try {
     try {
-      const res = await appContainer.apiv3Get('/page/exist-paths', { fromPath: path, toPath: newParentPath });
+      const res = await apiv3Get('/page/exist-paths', { fromPath: path, toPath: newParentPath });
       const { existPaths } = res.data;
       const { existPaths } = res.data;
       setExistingPaths(existPaths);
       setExistingPaths(existPaths);
     }
     }
@@ -112,12 +112,15 @@ const PageRenameModal = (props) => {
     setErrs(null);
     setErrs(null);
 
 
     try {
     try {
-      const response = await pageContainer.rename(
-        pageNameInput,
-        isRenameRecursively,
+      const response = await apiv3Put('/pages/rename', {
+        revisionId,
+        pageId,
+        isRecursively: isRenameRecursively,
         isRenameRedirect,
         isRenameRedirect,
-        isRenameMetadata,
-      );
+        isRemainMetadata,
+        newPagePath: pageNameInput,
+        path,
+      });
 
 
       const { page } = response.data;
       const { page } = response.data;
       const url = new URL(page.path, 'https://dummy');
       const url = new URL(page.path, 'https://dummy');
@@ -215,12 +218,12 @@ const PageRenameModal = (props) => {
           <input
           <input
             className="custom-control-input"
             className="custom-control-input"
             name="remain_metadata"
             name="remain_metadata"
-            id="cbRenameMetadata"
+            id="cbRemainMetadata"
             type="checkbox"
             type="checkbox"
-            checked={isRenameMetadata}
-            onChange={changeIsRenameMetadataHandler}
+            checked={isRemainMetadata}
+            onChange={changeIsRemainMetadataHandler}
           />
           />
-          <label className="custom-control-label" htmlFor="cbRenameMetadata">
+          <label className="custom-control-label" htmlFor="cbRemainMetadata">
             { t('modal_rename.label.Do not update metadata') }
             { t('modal_rename.label.Do not update metadata') }
             <p className="form-text text-muted mt-0">{ t('modal_rename.help.metadata') }</p>
             <p className="form-text text-muted mt-0">{ t('modal_rename.help.metadata') }</p>
           </label>
           </label>
@@ -244,17 +247,17 @@ const PageRenameModal = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const PageRenameModalWrapper = withUnstatedContainers(PageRenameModal, [AppContainer, PageContainer]);
-
+const PageRenameModalWrapper = withUnstatedContainers(PageRenameModal, [AppContainer]);
 
 
 PageRenameModal.propTypes = {
 PageRenameModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
   onClose: PropTypes.func.isRequired,
 
 
+  pageId: PropTypes.string.isRequired,
+  revisionId: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
 };
 };
 
 

+ 13 - 12
packages/app/src/components/PutbackPageModal.jsx

@@ -7,15 +7,13 @@ import {
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import { withUnstatedContainers } from './UnstatedUtils';
-
-import PageContainer from '~/client/services/PageContainer';
+import { apiPost } from '~/client/util/apiv1-client';
 
 
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 
 const PutBackPageModal = (props) => {
 const PutBackPageModal = (props) => {
   const {
   const {
-    t, isOpen, onClose, pageContainer, path,
+    t, isOpen, onClose, pageId, path,
   } = props;
   } = props;
 
 
   const [errs, setErrs] = useState(null);
   const [errs, setErrs] = useState(null);
@@ -30,7 +28,15 @@ const PutBackPageModal = (props) => {
     setErrs(null);
     setErrs(null);
 
 
     try {
     try {
-      const response = await pageContainer.revertRemove(isPutbackRecursively);
+      // control flag
+      // If is it not true, Request value must be `null`.
+      const recursively = isPutbackRecursively ? true : null;
+
+      const response = await apiPost('/pages.revertRemove', {
+        page_id: pageId,
+        recursively,
+      });
+
       const putbackPagePath = response.page.path;
       const putbackPagePath = response.page.path;
       window.location.href = encodeURI(putbackPagePath);
       window.location.href = encodeURI(putbackPagePath);
     }
     }
@@ -80,20 +86,15 @@ const PutBackPageModal = (props) => {
 
 
 };
 };
 
 
-/**
- * Wrapper component for using unstated
- */
-const PutBackPageModalWrapper = withUnstatedContainers(PutBackPageModal, [PageContainer]);
-
 PutBackPageModal.propTypes = {
 PutBackPageModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
   onClose: PropTypes.func.isRequired,
 
 
+  pageId: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
 };
 };
 
 
 
 
-export default withTranslation()(PutBackPageModalWrapper);
+export default withTranslation()(PutBackPageModal);

+ 15 - 11
packages/app/src/components/SearchPage.jsx

@@ -6,7 +6,6 @@ import { withTranslation } from 'react-i18next';
 
 
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 import SearchPageLayout from './SearchPage/SearchPageLayout';
 import SearchPageLayout from './SearchPage/SearchPageLayout';
 import SearchResultContent from './SearchPage/SearchResultContent';
 import SearchResultContent from './SearchPage/SearchResultContent';
@@ -30,23 +29,24 @@ class SearchPage extends React.Component {
       searchedKeyword: '',
       searchedKeyword: '',
       searchResults: [],
       searchResults: [],
       searchResultMeta: {},
       searchResultMeta: {},
-      focusedSearchResultData: {},
+      focusedSearchResultData: null,
       selectedPages: new Set(),
       selectedPages: new Set(),
       searchResultCount: 0,
       searchResultCount: 0,
       activePage: 1,
       activePage: 1,
-      pagingLimit: 10, // change to an appropriate limit number
+      pagingLimit: this.props.appContainer.config.pageLimitationL,
       excludeUsersHome: true,
       excludeUsersHome: true,
       excludeTrash: true,
       excludeTrash: true,
     };
     };
 
 
     this.changeURL = this.changeURL.bind(this);
     this.changeURL = this.changeURL.bind(this);
     this.search = this.search.bind(this);
     this.search = this.search.bind(this);
-    this.searchHandler = this.searchHandler.bind(this);
+    this.onSearchInvoked = this.onSearchInvoked.bind(this);
     this.selectPage = this.selectPage.bind(this);
     this.selectPage = this.selectPage.bind(this);
     this.toggleCheckBox = this.toggleCheckBox.bind(this);
     this.toggleCheckBox = this.toggleCheckBox.bind(this);
     this.onExcludeUsersHome = this.onExcludeUsersHome.bind(this);
     this.onExcludeUsersHome = this.onExcludeUsersHome.bind(this);
     this.onExcludeTrash = this.onExcludeTrash.bind(this);
     this.onExcludeTrash = this.onExcludeTrash.bind(this);
     this.onPagingNumberChanged = this.onPagingNumberChanged.bind(this);
     this.onPagingNumberChanged = this.onPagingNumberChanged.bind(this);
+    this.onPagingLimitChanged = this.onPagingLimitChanged.bind(this);
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
@@ -105,20 +105,23 @@ class SearchPage extends React.Component {
    * this method is called when user changes paging number
    * this method is called when user changes paging number
    */
    */
   async onPagingNumberChanged(activePage) {
   async onPagingNumberChanged(activePage) {
-    // this.setState does not change the state immediately and following calls of this.search outside of this.setState will have old activePage state.
-    // To prevent above, pass this.search as a callback function to make sure this.search will have the latest activePage state.
     this.setState({ activePage }, () => this.search({ keyword: this.state.searchedKeyword }));
     this.setState({ activePage }, () => this.search({ keyword: this.state.searchedKeyword }));
   }
   }
 
 
   /**
   /**
    * this method is called when user searches by pressing Enter or using searchbox
    * this method is called when user searches by pressing Enter or using searchbox
    */
    */
-  async searchHandler(data) {
-    // this.setState does not change the state immediately and following calls of this.search outside of this.setState will have old activePage state.
-    // To prevent above, pass this.search as a callback function to make sure this.search will have the latest activePage state.
+  async onSearchInvoked(data) {
     this.setState({ activePage: 1 }, () => this.search(data));
     this.setState({ activePage: 1 }, () => this.search(data));
   }
   }
 
 
+  /**
+   * change number of pages to display per page and execute search method after.
+   */
+  async onPagingLimitChanged(limit) {
+    this.setState({ pagingLimit: limit }, () => this.search({ keyword: this.state.searchedKeyword }));
+  }
+
   async search(data) {
   async search(data) {
     const keyword = data.keyword;
     const keyword = data.keyword;
     if (keyword === '') {
     if (keyword === '') {
@@ -223,7 +226,7 @@ class SearchPage extends React.Component {
       <SearchControl
       <SearchControl
         searchingKeyword={this.state.searchingKeyword}
         searchingKeyword={this.state.searchingKeyword}
         appContainer={this.props.appContainer}
         appContainer={this.props.appContainer}
-        onSearchInvoked={this.searchHandler}
+        onSearchInvoked={this.onSearchInvoked}
         onExcludeUsersHome={this.onExcludeUsersHome}
         onExcludeUsersHome={this.onExcludeUsersHome}
         onExcludeTrash={this.onExcludeTrash}
         onExcludeTrash={this.onExcludeTrash}
       >
       >
@@ -240,6 +243,8 @@ class SearchPage extends React.Component {
           SearchResultContent={this.renderSearchResultContent}
           SearchResultContent={this.renderSearchResultContent}
           searchResultMeta={this.state.searchResultMeta}
           searchResultMeta={this.state.searchResultMeta}
           searchingKeyword={this.state.searchedKeyword}
           searchingKeyword={this.state.searchedKeyword}
+          onPagingLimitChanged={this.onPagingLimitChanged}
+          initialPagingLimit={this.props.appContainer.config.pageLimitationL || 50}
         >
         >
         </SearchPageLayout>
         </SearchPageLayout>
       </div>
       </div>
@@ -256,7 +261,6 @@ const SearchPageWrapper = withUnstatedContainers(SearchPage, [AppContainer]);
 SearchPage.propTypes = {
 SearchPage.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
   query: PropTypes.object,
   query: PropTypes.object,
 };
 };
 SearchPage.defaultProps = {
 SearchPage.defaultProps = {

+ 1 - 1
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -62,7 +62,7 @@ const SearchControl: FC <Props> = (props: Props) => {
         </div>
         </div>
       </div>
       </div>
       {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
       {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
-      <div className="search-control d-flex align-items-center py-3 border-bottom border-gray">
+      <div className="search-control d-flex align-items-center py-2 border-bottom border-gray">
         <div className="d-flex mr-auto ml-4">
         <div className="d-flex mr-auto ml-4">
           {/* Todo: design will be fixed in #80324. Function will be implemented in #77525 */}
           {/* Todo: design will be fixed in #80324. Function will be implemented in #77525 */}
           <DeleteSelectedPageGroup
           <DeleteSelectedPageGroup

+ 15 - 3
packages/app/src/components/SearchPage/SearchPageLayout.tsx

@@ -12,7 +12,9 @@ type Props = {
   SearchResultList: React.FunctionComponent,
   SearchResultList: React.FunctionComponent,
   SearchResultContent: React.FunctionComponent,
   SearchResultContent: React.FunctionComponent,
   searchResultMeta: SearchResultMeta,
   searchResultMeta: SearchResultMeta,
-  searchingKeyword: string
+  searchingKeyword: string,
+  initialPagingLimit: number,
+  onPagingLimitChanged: (limit: number) => void
 }
 }
 
 
 const SearchPageLayout: FC<Props> = (props: Props) => {
 const SearchPageLayout: FC<Props> = (props: Props) => {
@@ -27,13 +29,23 @@ const SearchPageLayout: FC<Props> = (props: Props) => {
         <div className="flex-grow-1 flex-basis-0 page-list border boder-gray search-result-list" id="search-result-list">
         <div className="flex-grow-1 flex-basis-0 page-list border boder-gray search-result-list" id="search-result-list">
 
 
           <SearchControl></SearchControl>
           <SearchControl></SearchControl>
-          <div className="d-flex align-items-start justify-content-between mt-3 ml-4">
-            <div className="search-result-meta">
+          <div className="d-flex align-items-center justify-content-between my-3 ml-4">
+            <div className="search-result-meta text-nowrap mr-3">
               <span className="font-weight-light">{t('search_result.result_meta')} </span>
               <span className="font-weight-light">{t('search_result.result_meta')} </span>
               <span className="h5">{`"${searchingKeyword}"`}</span>
               <span className="h5">{`"${searchingKeyword}"`}</span>
               {/* Todo: replace "1-10" to the appropriate value */}
               {/* Todo: replace "1-10" to the appropriate value */}
               <span className="ml-3">1-10 / {searchResultMeta.total || 0}</span>
               <span className="ml-3">1-10 / {searchResultMeta.total || 0}</span>
             </div>
             </div>
+            <div className="input-group search-result-select-group">
+              <div className="input-group-prepend">
+                <label className="input-group-text text-secondary" htmlFor="inputGroupSelect01">{t('search_result.number_of_list_to_display')}</label>
+              </div>
+              <select className="custom-select" id="inputGroupSelect01" onChange={(e) => { props.onPagingLimitChanged(Number(e.target.value)) }}>
+                {[20, 50, 100, 200].map((limit) => {
+                  return <option selected={limit === props.initialPagingLimit} value={limit}>{limit}{t('search_result.page_number_unit')}</option>;
+                })}
+              </select>
+            </div>
           </div>
           </div>
 
 
           <div className="page-list">
           <div className="page-list">

+ 25 - 31
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -12,40 +12,34 @@ type Props ={
   focusedSearchResultData : IPageSearchResultData,
   focusedSearchResultData : IPageSearchResultData,
 }
 }
 const SearchResultContent: FC<Props> = (props: Props) => {
 const SearchResultContent: FC<Props> = (props: Props) => {
+  const page = props.focusedSearchResultData?.pageData || {};
+  if (page == null) return null;
   // Temporaly workaround for lint error
   // Temporaly workaround for lint error
   // later needs to be fixed: RevisoinRender to typescriptcomponet
   // later needs to be fixed: RevisoinRender to typescriptcomponet
-  const RevisionRenderTypeAny: any = RevisionLoader;
-  const renderPage = (searchResultData) => {
-    const page = searchResultData?.pageData || {};
-    const growiRenderer = props.appContainer.getRenderer('searchresult');
-    let showTags = false;
-    if (page.tags != null && page.tags.length > 0) { showTags = true }
-    return (
-      <div key={page._id} className="search-result-page mb-5">
-        <h2>
-          <a href={page.path} className="text-break">
-            {page.path}
-          </a>
-          {showTags && (
-            <div className="mt-1 small">
-              <i className="tag-icon icon-tag"></i> {page.tags.join(', ')}
-            </div>
-          )}
-        </h2>
-        <RevisionRenderTypeAny
-          growiRenderer={growiRenderer}
-          pageId={page._id}
-          pagePath={page.path}
-          revisionId={page.revision}
-          highlightKeywords={props.searchingKeyword}
-        />
-      </div>
-    );
-  };
-  const content = renderPage(props.focusedSearchResultData);
+  const RevisionLoaderTypeAny: any = RevisionLoader;
+  const growiRenderer = props.appContainer.getRenderer('searchresult');
+  let showTags = false;
+  if (page.tags != null && page.tags.length > 0) { showTags = true }
   return (
   return (
-
-    <div>{content}</div>
+    <div key={page._id} className="search-result-page mb-5">
+      <h2>
+        <a href={page.path} className="text-break">
+          {page.path}
+        </a>
+        {showTags && (
+          <div className="mt-1 small">
+            <i className="tag-icon icon-tag"></i> {page.tags?.join(', ')}
+          </div>
+        )}
+      </h2>
+      <RevisionLoaderTypeAny
+        growiRenderer={growiRenderer}
+        pageId={page._id}
+        pagePath={page.path}
+        revisionId={page.revision}
+        highlightKeywords={props.searchingKeyword}
+      />
+    </div>
   );
   );
 };
 };
 
 

+ 2 - 1
packages/app/src/components/SearchPage/SearchResultListItem.tsx

@@ -76,8 +76,9 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
   // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
   // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
   const pageId = `#${pageData._id}`;
   const pageId = `#${pageData._id}`;
 
 
+  const isPathIncludedHtml = pageMeta.elasticSearchResult.highlightedPath != null;
   const dPagePath = new DevidedPagePath(pageData.path, false, true);
   const dPagePath = new DevidedPagePath(pageData.path, false, true);
-  const pagePathElem = <PagePathLabel page={pageData} isFormerOnly />;
+  const pagePathElem = <PagePathLabel path={pageMeta.elasticSearchResult.highlightedPath} isFormerOnly isPathIncludedHtml={isPathIncludedHtml} />;
 
 
   const onClickInvoked = (pageId) => {
   const onClickInvoked = (pageId) => {
     if (props.onClickInvoked != null) {
     if (props.onClickInvoked != null) {

+ 1 - 1
packages/app/src/components/SearchTypeahead.jsx

@@ -180,7 +180,7 @@ class SearchTypeahead extends React.Component {
     return (
     return (
       <span>
       <span>
         <UserPicture user={page.lastUpdateUser} size="sm" noLink />
         <UserPicture user={page.lastUpdateUser} size="sm" noLink />
-        <span className="ml-1 text-break text-wrap"><PagePathLabel page={page} /></span>
+        <span className="ml-1 text-break text-wrap"><PagePathLabel path={page.path} /></span>
         <PageListMeta page={page} />
         <PageListMeta page={page} />
       </span>
       </span>
     );
     );

+ 1 - 1
packages/app/src/interfaces/search.ts

@@ -12,7 +12,7 @@ export type IPageSearchResultData = {
     bookmarkCount: number,
     bookmarkCount: number,
     elasticSearchResult: {
     elasticSearchResult: {
       snippet: string,
       snippet: string,
-      matchedPath: string,
+      highlightedPath: string,
     },
     },
   },
   },
 }
 }

+ 1 - 0
packages/app/src/server/models/config.ts

@@ -235,6 +235,7 @@ schema.statics.getLocalconfig = function(crowi) {
     isSearchServiceReachable: crowi.searchService.isReachable,
     isSearchServiceReachable: crowi.searchService.isReachable,
     isMailerSetup: crowi.mailService.isMailerSetup,
     isMailerSetup: crowi.mailService.isMailerSetup,
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
+    pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
   };
   };
 
 
   return localConfig;
   return localConfig;

+ 1 - 2
packages/app/src/server/service/search.js

@@ -170,8 +170,7 @@ class SearchService {
 
 
       data.elasticSearchResult = {
       data.elasticSearchResult = {
         snippet: filterXss.process(snippet),
         snippet: filterXss.process(snippet),
-        // todo: use filter xss.process() for matchedPath;
-        matchedPath: pathMatch,
+        highlightedPath: filterXss.process(pathMatch),
       };
       };
     });
     });
     return esResult;
     return esResult;

+ 5 - 2
packages/app/src/styles/_search.scss

@@ -207,10 +207,13 @@
     }
     }
 
 
     .search-result-meta {
     .search-result-meta {
-      margin-bottom: 10px;
       font-weight: bold;
       font-weight: bold;
     }
     }
-
+    .search-result-select-group {
+      > select {
+        max-width: 8rem;
+      }
+    }
     .search-result-list-delete-checkbox {
     .search-result-list-delete-checkbox {
       margin: 0 10px 0 0;
       margin: 0 10px 0 0;
       vertical-align: middle;
       vertical-align: middle;

+ 2 - 2
packages/core/src/models/devided-page-path.js

@@ -2,8 +2,8 @@ import * as pathUtils from '../utils/path-utils';
 
 
 // https://regex101.com/r/BahpKX/2
 // https://regex101.com/r/BahpKX/2
 const PATTERN_INCLUDE_DATE = /^(.+\/[^/]+)\/(\d{4}|\d{4}\/\d{2}|\d{4}\/\d{2}\/\d{2})$/;
 const PATTERN_INCLUDE_DATE = /^(.+\/[^/]+)\/(\d{4}|\d{4}\/\d{2}|\d{4}\/\d{2}\/\d{2})$/;
-// https://regex101.com/r/WVpPpY/1
-const PATTERN_DEFAULT = /^((.*)\/)?([^/]+)$/;
+// https://regex101.com/r/HJNvMW/1
+const PATTERN_DEFAULT = /^((.*)(?<!<)\/)?(.+)$/;
 
 
 export class DevidedPagePath {
 export class DevidedPagePath {
 
 

+ 1 - 1
packages/plugin-lsx/src/client/js/components/LsxPageList/PagePathWrapper.jsx

@@ -13,7 +13,7 @@ export class PagePathWrapper extends React.Component {
     }
     }
 
 
     return (
     return (
-      <PagePathLabel page={{ path: this.props.pagePath }} isLatterOnly additionalClassNames={classNames} />
+      <PagePathLabel path={this.props.pagePath} isLatterOnly additionalClassNames={classNames} />
     );
     );
   }
   }
 
 

+ 0 - 40
packages/ui/src/components/PagePath/PagePathLabel.jsx

@@ -1,40 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { DevidedPagePath } from '@growi/core';
-
-export const PagePathLabel = (props) => {
-
-  const dPagePath = new DevidedPagePath(props.page.path, false, true);
-
-  let classNames = [''];
-  classNames = classNames.concat(props.additionalClassNames);
-
-  if (props.isLatterOnly) {
-    return <span className={classNames.join(' ')}>{dPagePath.latter}</span>;
-  }
-
-  if (props.isFormerOnly) {
-    const textElem = dPagePath.isFormerRoot
-      ? <>/</>
-      : <>{dPagePath.former}</>;
-    return <span className={classNames.join(' ')}>{textElem}</span>;
-  }
-
-  const textElem = dPagePath.isRoot
-    ? <><strong>/</strong></>
-    : <>{dPagePath.former}/<strong>{dPagePath.latter}</strong></>;
-
-  return <span className={classNames.join(' ')}>{textElem}</span>;
-};
-
-PagePathLabel.propTypes = {
-  page: PropTypes.object.isRequired,
-  isLatterOnly: PropTypes.bool,
-  isFormerOnly: PropTypes.bool,
-  additionalClassNames: PropTypes.arrayOf(PropTypes.string),
-};
-
-PagePathLabel.defaultProps = {
-  additionalClassNames: [],
-};

+ 56 - 0
packages/ui/src/components/PagePath/PagePathLabel.tsx

@@ -0,0 +1,56 @@
+import React, { FC } from 'react';
+
+import { DevidedPagePath } from '@growi/core';
+
+
+type TextElemProps = {
+  children?: React.ReactNode
+  isHTML?: boolean,
+}
+
+const TextElement: FC<TextElemProps> = (props: TextElemProps) => (
+  <>
+    { props.isHTML
+      // eslint-disable-next-line react/no-danger
+      ? <span dangerouslySetInnerHTML={{ __html: props.children?.toString() || '' }}></span>
+      : <>{props.children}</>
+    }
+  </>
+);
+
+
+type Props = {
+  path: string,
+  isLatterOnly?: boolean,
+  isFormerOnly?: boolean,
+  isPathIncludedHtml?: boolean,
+  additionalClassNames?: string[],
+}
+
+export const PagePathLabel: FC<Props> = (props:Props) => {
+  const {
+    isLatterOnly, isFormerOnly, isPathIncludedHtml, additionalClassNames, path,
+  } = props;
+
+  const dPagePath = new DevidedPagePath(path, false, true);
+
+  const classNames = additionalClassNames || [];
+
+  let textElem;
+
+  if (isLatterOnly) {
+    textElem = <TextElement isHTML={isPathIncludedHtml}>{dPagePath.latter}</TextElement>;
+  }
+  else if (isFormerOnly) {
+    textElem = dPagePath.isFormerRoot
+      ? <>/</>
+      : <TextElement isHTML={isPathIncludedHtml}>{dPagePath.former}</TextElement>;
+  }
+  else {
+    textElem = dPagePath.isRoot
+      ? <strong>/</strong>
+      : <TextElement isHTML={isPathIncludedHtml}>{dPagePath.former}/<strong>{dPagePath.latter}</strong></TextElement>;
+  }
+
+  return <span className={classNames.join(' ')}>{textElem}</span>;
+};