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

Merge pull request #5224 from weseek/imprv/notfound-page

imprv: Search page and NotFound page
Yuki Takei 4 лет назад
Родитель
Сommit
6ea833603f
34 измененных файлов с 337 добавлено и 345 удалено
  1. 1 1
      packages/app/src/client/app.jsx
  2. 2 2
      packages/app/src/components/Admin/UserGroupDetail/UserGroupPageList.jsx
  3. 1 1
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  4. 56 0
      packages/app/src/components/DescendantsPageList.tsx
  5. 20 17
      packages/app/src/components/ForbiddenPage.tsx
  6. 2 2
      packages/app/src/components/IdenticalPathPage.tsx
  7. 4 3
      packages/app/src/components/Navbar/GlobalSearch.tsx
  8. 8 12
      packages/app/src/components/NotFoundPage.tsx
  9. 12 10
      packages/app/src/components/Page/DisplaySwitcher.jsx
  10. 1 5
      packages/app/src/components/PageAccessories.jsx
  11. 10 9
      packages/app/src/components/PageAccessoriesModal.jsx
  12. 5 7
      packages/app/src/components/PageAccessoriesModalControl.jsx
  13. 0 102
      packages/app/src/components/PageList.jsx
  14. 2 2
      packages/app/src/components/PageList/BookmarkList.jsx
  15. 49 0
      packages/app/src/components/PageList/PageList.tsx
  16. 28 27
      packages/app/src/components/PageList/PageListItemL.tsx
  17. 3 3
      packages/app/src/components/PageList/PageListItemS.jsx
  18. 2 2
      packages/app/src/components/RecentCreated/RecentCreated.jsx
  19. 3 2
      packages/app/src/components/SearchForm.tsx
  20. 1 1
      packages/app/src/components/SearchPage.jsx
  21. 3 3
      packages/app/src/components/SearchPage/SearchPageLayout.tsx
  22. 3 5
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  23. 11 9
      packages/app/src/components/SearchPage/SearchResultList.tsx
  24. 5 4
      packages/app/src/components/SearchTypeahead.tsx
  25. 2 2
      packages/app/src/components/TrashPageList.jsx
  26. 15 0
      packages/app/src/interfaces/page.ts
  27. 11 11
      packages/app/src/interfaces/search.ts
  28. 0 7
      packages/app/src/server/routes/page.js
  29. 1 0
      packages/app/src/server/views/layout-growi/not_found.html
  30. 0 3
      packages/app/src/server/views/widget/page_alerts.html
  31. 22 0
      packages/app/src/styles/_page_list.scss
  32. 4 46
      packages/app/src/styles/_search.scss
  33. 13 18
      packages/app/src/styles/theme/_apply-colors-dark.scss
  34. 37 29
      packages/app/src/styles/theme/_apply-colors.scss

+ 1 - 1
packages/app/src/client/app.jsx

@@ -101,7 +101,7 @@ Object.assign(componentMappings, {
 
 
   'not-found-page': <NotFoundPage />,
   'not-found-page': <NotFoundPage />,
 
 
-  'forbidden-page': <ForbiddenPage />,
+  'forbidden-page': <ForbiddenPage isSharePage={appContainer.config.disableLinkSharing} />,
 
 
   'page-timeline': <PageTimeline />,
   'page-timeline': <PageTimeline />,
 
 

+ 2 - 2
packages/app/src/components/Admin/UserGroupDetail/UserGroupPageList.jsx

@@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import Page from '../../PageList/Page';
+import PageListItemS from '../../PageList/PageListItemS';
 import PaginationWrapper from '../../PaginationWrapper';
 import PaginationWrapper from '../../PaginationWrapper';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
@@ -57,7 +57,7 @@ class UserGroupPageList extends React.Component {
     return (
     return (
       <Fragment>
       <Fragment>
         <ul className="page-list-ul page-list-ul-flat mb-3">
         <ul className="page-list-ul page-list-ul-flat mb-3">
-          {this.state.currentPages.map(page => <li key={page._id}><Page page={page} /></li>)}
+          {this.state.currentPages.map(page => <li key={page._id}><PageListItemS page={page} /></li>)}
         </ul>
         </ul>
         {relatedPages.length === 0 ? <p>{t('admin:user_group_management.no_pages')}</p> : (
         {relatedPages.length === 0 ? <p>{t('admin:user_group_management.no_pages')}</p> : (
           <PaginationWrapper
           <PaginationWrapper

+ 1 - 1
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -13,7 +13,7 @@ import { useSWRBookmarkInfo } from '~/stores/bookmark';
 
 
 type PageItemControlProps = {
 type PageItemControlProps = {
   page: Partial<IPageHasId>
   page: Partial<IPageHasId>
-  isEnableActions: boolean
+  isEnableActions?: boolean
   isDeletable: boolean
   isDeletable: boolean
   onClickDeleteButtonHandler?: (pageId: string) => void
   onClickDeleteButtonHandler?: (pageId: string) => void
   onClickRenameButtonHandler?: (pageId: string) => void
   onClickRenameButtonHandler?: (pageId: string) => void

+ 56 - 0
packages/app/src/components/DescendantsPageList.tsx

@@ -0,0 +1,56 @@
+import React, { useState } from 'react';
+
+import { useSWRxPageList } from '~/stores/page';
+
+import PageList from './PageList/PageList';
+import PaginationWrapper from './PaginationWrapper';
+
+type Props = {
+  path: string,
+}
+
+const DescendantsPageList = (props: Props): JSX.Element => {
+  const { path } = props;
+
+  const [activePage, setActivePage] = useState(1);
+
+  const { data, error } = useSWRxPageList(path, activePage);
+
+  function setPageNumber(selectedPageNumber) {
+    setActivePage(selectedPageNumber);
+  }
+
+  if (error != null) {
+    return (
+      <div className="my-5">
+        <div className="text-danger">{error.message}</div>
+      </div>
+    );
+  }
+
+  if (data === undefined) {
+    return (
+      <div className="wiki">
+        <div className="text-muted text-center">
+          <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <>
+      <PageList pages={data} />
+
+      <PaginationWrapper
+        activePage={activePage}
+        changePage={setPageNumber}
+        totalItemsCount={data.totalCount}
+        pagingLimit={data.limit}
+        align="center"
+      />
+    </>
+  );
+};
+
+export default DescendantsPageList;

+ 20 - 17
packages/app/src/components/ForbiddenPage.jsx → packages/app/src/components/ForbiddenPage.tsx

@@ -1,19 +1,23 @@
 import React, { useMemo } from 'react';
 import React, { useMemo } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
+
 import PageListIcon from './Icons/PageListIcon';
 import PageListIcon from './Icons/PageListIcon';
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
-import PageList from './PageList';
+import DescendantsPageList from './DescendantsPageList';
+
 
 
+type Props = {
+  isSharePage?: boolean,
+}
 
 
-const ForbiddenPage = (props) => {
-  const { t } = props;
+const ForbiddenPage = React.memo((props: Props): JSX.Element => {
+  const { t } = useTranslation();
 
 
   const navTabMapping = useMemo(() => {
   const navTabMapping = useMemo(() => {
     return {
     return {
       pagelist: {
       pagelist: {
         Icon: PageListIcon,
         Icon: PageListIcon,
-        Content: PageList,
+        Content: DescendantsPageList,
         i18n: t('page_list'),
         i18n: t('page_list'),
         index: 0,
         index: 0,
       },
       },
@@ -31,24 +35,23 @@ const ForbiddenPage = (props) => {
         </div>
         </div>
       </div>
       </div>
 
 
-
       <div className="row row-alerts d-edit-none">
       <div className="row row-alerts d-edit-none">
         <div className="col-sm-12">
         <div className="col-sm-12">
           <p className="alert alert-primary py-3 px-4">
           <p className="alert alert-primary py-3 px-4">
             <i className="icon-fw icon-lock" aria-hidden="true" />
             <i className="icon-fw icon-lock" aria-hidden="true" />
-            {t('Browsing of this page is restricted')}
+            { props.isSharePage ? t('custom_navigation.link_sharing_is_disabled') : t('Browsing of this page is restricted')}
           </p>
           </p>
         </div>
         </div>
       </div>
       </div>
-      <div className="mt-5">
-        <CustomNavAndContents navTabMapping={navTabMapping} />
-      </div>
+
+      { !props.isSharePage && (
+        <div className="mt-5">
+          <CustomNavAndContents navTabMapping={navTabMapping} />
+        </div>
+      ) }
+
     </>
     </>
   );
   );
-};
-
-ForbiddenPage.propTypes = {
-  t: PropTypes.func.isRequired,
-};
+});
 
 
-export default withTranslation()(ForbiddenPage);
+export default ForbiddenPage;

+ 2 - 2
packages/app/src/components/IdenticalPathPage.tsx

@@ -7,7 +7,7 @@ import { DevidedPagePath } from '@growi/core';
 
 
 import { useCurrentPagePath } from '~/stores/context';
 import { useCurrentPagePath } from '~/stores/context';
 
 
-import PageListItem from './Page/PageListItem';
+import { PageListItemL } from './PageList/PageListItemL';
 
 
 
 
 type IdenticalPathAlertProps = {
 type IdenticalPathAlertProps = {
@@ -79,7 +79,7 @@ const IdenticalPathPage:FC<IdenticalPathPageProps> = (props: IdenticalPathPagePr
           <ul className="page-list-ul list-group-flush border px-3">
           <ul className="page-list-ul list-group-flush border px-3">
             {pageDataList.map((data) => {
             {pageDataList.map((data) => {
               return (
               return (
-                <PageListItem
+                <PageListItemL
                   key={data.pageData._id}
                   key={data.pageData._id}
                   page={data}
                   page={data}
                   isSelected={false}
                   isSelected={false}

+ 4 - 3
packages/app/src/components/Navbar/GlobalSearch.tsx

@@ -5,13 +5,14 @@ import { useTranslation } from 'react-i18next';
 import assert from 'assert';
 import assert from 'assert';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import { IPageSearchResultData } from '~/interfaces/search';
 import { IFocusable } from '~/client/interfaces/focusable';
 import { IFocusable } from '~/client/interfaces/focusable';
+import { useGlobalSearchFormRef } from '~/stores/ui';
+import { IPageSearchMeta } from '~/interfaces/search';
+import { IPageWithMeta } from '~/interfaces/page';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import SearchForm from '../SearchForm';
 import SearchForm from '../SearchForm';
-import { useGlobalSearchFormRef } from '~/stores/ui';
 
 
 
 
 type Props = {
 type Props = {
@@ -32,7 +33,7 @@ const GlobalSearch: FC<Props> = (props: Props) => {
   const [isScopeChildren, setScopeChildren] = useState<boolean>(appContainer.getConfig().isSearchScopeChildrenAsDefault);
   const [isScopeChildren, setScopeChildren] = useState<boolean>(appContainer.getConfig().isSearchScopeChildrenAsDefault);
   const [isFocused, setFocused] = useState<boolean>(false);
   const [isFocused, setFocused] = useState<boolean>(false);
 
 
-  const gotoPage = useCallback((data: IPageSearchResultData[]) => {
+  const gotoPage = useCallback((data: IPageWithMeta<IPageSearchMeta>[]) => {
     assert(data.length > 0);
     assert(data.length > 0);
 
 
     const page = data[0].pageData; // should be single page selected
     const page = data[0].pageData; // should be single page selected

+ 8 - 12
packages/app/src/components/NotFoundPage.jsx → packages/app/src/components/NotFoundPage.tsx

@@ -1,20 +1,20 @@
 import React, { useMemo } from 'react';
 import React, { useMemo } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
+
 import PageListIcon from './Icons/PageListIcon';
 import PageListIcon from './Icons/PageListIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
-import PageList from './PageList';
+import DescendantsPageList from './DescendantsPageList';
 import PageTimeline from './PageTimeline';
 import PageTimeline from './PageTimeline';
 
 
-const NotFoundPage = (props) => {
-  const { t } = props;
+const NotFoundPage = (): JSX.Element => {
+  const { t } = useTranslation();
 
 
   const navTabMapping = useMemo(() => {
   const navTabMapping = useMemo(() => {
     return {
     return {
       pagelist: {
       pagelist: {
         Icon: PageListIcon,
         Icon: PageListIcon,
-        Content: PageList,
+        Content: DescendantsPageList,
         i18n: t('page_list'),
         i18n: t('page_list'),
         index: 0,
         index: 0,
       },
       },
@@ -29,14 +29,10 @@ const NotFoundPage = (props) => {
 
 
 
 
   return (
   return (
-    <div className="mt-5 d-edit-none">
+    <div className="d-edit-none">
       <CustomNavAndContents navTabMapping={navTabMapping} />
       <CustomNavAndContents navTabMapping={navTabMapping} />
     </div>
     </div>
   );
   );
 };
 };
 
 
-NotFoundPage.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-};
-
-export default withTranslation()(NotFoundPage);
+export default NotFoundPage;

+ 12 - 10
packages/app/src/components/Page/DisplaySwitcher.jsx

@@ -35,20 +35,22 @@ const DisplaySwitcher = (props) => {
         <TabPane tabId={EditorMode.View}>
         <TabPane tabId={EditorMode.View}>
           <div className="d-flex flex-column flex-lg-row-reverse">
           <div className="d-flex flex-column flex-lg-row-reverse">
 
 
-            <div className="grw-side-contents-container">
-              <div className="grw-side-contents-sticky-container">
-                <div className="border-bottom pb-1">
-                  <PageAccessories isNotFoundPage={!isPageExist} />
-                </div>
+            { isPageExist && (
+              <div className="grw-side-contents-container">
+                <div className="grw-side-contents-sticky-container">
+                  <div className="border-bottom pb-1">
+                    <PageAccessories />
+                  </div>
 
 
-                <div className="d-none d-lg-block">
-                  <div id="revision-toc" className="revision-toc">
-                    <TableOfContents />
+                  <div className="d-none d-lg-block">
+                    <div id="revision-toc" className="revision-toc">
+                      <TableOfContents />
+                    </div>
+                    <ContentLinkButtons />
                   </div>
                   </div>
-                  <ContentLinkButtons />
                 </div>
                 </div>
               </div>
               </div>
-            </div>
+            ) }
 
 
             <div className="flex-grow-1 flex-basis-0 mw-0">
             <div className="flex-grow-1 flex-basis-0 mw-0">
               {pageUser && <UserInfo pageUser={pageUser} />}
               {pageUser && <UserInfo pageUser={pageUser} />}

+ 1 - 5
packages/app/src/components/PageAccessories.jsx

@@ -9,7 +9,7 @@ import AppContainer from '~/client/services/AppContainer';
 import PageAccessoriesContainer from '~/client/services/PageAccessoriesContainer';
 import PageAccessoriesContainer from '~/client/services/PageAccessoriesContainer';
 
 
 const PageAccessories = (props) => {
 const PageAccessories = (props) => {
-  const { appContainer, pageAccessoriesContainer, isNotFoundPage } = props;
+  const { appContainer, pageAccessoriesContainer } = props;
   const { isGuestUser, isSharedUser } = appContainer;
   const { isGuestUser, isSharedUser } = appContainer;
 
 
   return (
   return (
@@ -17,12 +17,10 @@ const PageAccessories = (props) => {
       <PageAccessoriesModalControl
       <PageAccessoriesModalControl
         isGuestUser={isGuestUser}
         isGuestUser={isGuestUser}
         isSharedUser={isSharedUser}
         isSharedUser={isSharedUser}
-        isNotFoundPage={isNotFoundPage}
       />
       />
       <PageAccessoriesModal
       <PageAccessoriesModal
         isGuestUser={isGuestUser}
         isGuestUser={isGuestUser}
         isSharedUser={isSharedUser}
         isSharedUser={isSharedUser}
-        isNotFoundPage={isNotFoundPage}
         isOpen={pageAccessoriesContainer.state.isPageAccessoriesModalShown}
         isOpen={pageAccessoriesContainer.state.isPageAccessoriesModalShown}
         onClose={pageAccessoriesContainer.closePageAccessoriesModal}
         onClose={pageAccessoriesContainer.closePageAccessoriesModal}
       />
       />
@@ -37,8 +35,6 @@ const PageAccessoriesWrapper = withUnstatedContainers(PageAccessories, [AppConta
 PageAccessories.propTypes = {
 PageAccessories.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
-
-  isNotFoundPage: PropTypes.bool.isRequired,
 };
 };
 
 
 export default PageAccessoriesWrapper;
 export default PageAccessoriesWrapper;

+ 10 - 9
packages/app/src/components/PageAccessoriesModal.jsx

@@ -13,10 +13,11 @@ import AttachmentIcon from './Icons/AttachmentIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 
 
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
+import PageContainer from '~/client/services/PageContainer';
 import PageAccessoriesContainer from '~/client/services/PageAccessoriesContainer';
 import PageAccessoriesContainer from '~/client/services/PageAccessoriesContainer';
 import PageAttachment from './PageAttachment';
 import PageAttachment from './PageAttachment';
 import PageTimeline from './PageTimeline';
 import PageTimeline from './PageTimeline';
-import PageList from './PageList';
+import DescendantsPageList from './DescendantsPageList';
 import PageHistory from './PageHistory';
 import PageHistory from './PageHistory';
 import ShareLink from './ShareLink/ShareLink';
 import ShareLink from './ShareLink/ShareLink';
 import { CustomNavTab } from './CustomNavigation/CustomNav';
 import { CustomNavTab } from './CustomNavigation/CustomNav';
@@ -24,7 +25,7 @@ import ExpandOrContractButton from './ExpandOrContractButton';
 
 
 const PageAccessoriesModal = (props) => {
 const PageAccessoriesModal = (props) => {
   const {
   const {
-    t, pageAccessoriesContainer, onClose, isGuestUser, isSharedUser, isNotFoundPage,
+    t, pageContainer, pageAccessoriesContainer, onClose, isGuestUser, isSharedUser,
   } = props;
   } = props;
   const isLinkSharingDisabled = pageAccessoriesContainer.appContainer.config.disableLinkSharing;
   const isLinkSharingDisabled = pageAccessoriesContainer.appContainer.config.disableLinkSharing;
   const { switchActiveTab } = pageAccessoriesContainer;
   const { switchActiveTab } = pageAccessoriesContainer;
@@ -49,22 +50,21 @@ const PageAccessoriesModal = (props) => {
         Icon: HistoryIcon,
         Icon: HistoryIcon,
         i18n: t('History'),
         i18n: t('History'),
         index: 2,
         index: 2,
-        isLinkEnabled: v => !isGuestUser && !isSharedUser && !isNotFoundPage,
+        isLinkEnabled: v => !isGuestUser && !isSharedUser,
       },
       },
       attachment: {
       attachment: {
         Icon: AttachmentIcon,
         Icon: AttachmentIcon,
         i18n: t('attachment_data'),
         i18n: t('attachment_data'),
         index: 3,
         index: 3,
-        isLinkEnabled: v => !isNotFoundPage,
       },
       },
       shareLink: {
       shareLink: {
         Icon: ShareLinkIcon,
         Icon: ShareLinkIcon,
         i18n: t('share_links.share_link_management'),
         i18n: t('share_links.share_link_management'),
         index: 4,
         index: 4,
-        isLinkEnabled: v => !isGuestUser && !isSharedUser && !isNotFoundPage && !isLinkSharingDisabled,
+        isLinkEnabled: v => !isGuestUser && !isSharedUser && !isLinkSharingDisabled,
       },
       },
     };
     };
-  }, [t, isGuestUser, isSharedUser, isNotFoundPage, isLinkSharingDisabled]);
+  }, [t, isGuestUser, isSharedUser, isLinkSharingDisabled]);
 
 
   const closeModalHandler = useCallback(() => {
   const closeModalHandler = useCallback(() => {
     if (onClose == null) {
     if (onClose == null) {
@@ -116,7 +116,7 @@ const PageAccessoriesModal = (props) => {
               the 'navTabMapping[tabId].Content' for PageAccessoriesModal depends on activeComponents */}
               the 'navTabMapping[tabId].Content' for PageAccessoriesModal depends on activeComponents */}
           <TabContent activeTab={activeTab}>
           <TabContent activeTab={activeTab}>
             <TabPane tabId="pagelist">
             <TabPane tabId="pagelist">
-              {activeComponents.has('pagelist') && <PageList />}
+              {activeComponents.has('pagelist') && <DescendantsPageList path={pageContainer.state.path} />}
             </TabPane>
             </TabPane>
             <TabPane tabId="timeline">
             <TabPane tabId="timeline">
               {activeComponents.has('timeline') && <PageTimeline /> }
               {activeComponents.has('timeline') && <PageTimeline /> }
@@ -144,14 +144,15 @@ const PageAccessoriesModal = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const PageAccessoriesModalWrapper = withUnstatedContainers(PageAccessoriesModal, [PageAccessoriesContainer]);
+const PageAccessoriesModalWrapper = withUnstatedContainers(PageAccessoriesModal, [PageContainer, PageAccessoriesContainer]);
 
 
 PageAccessoriesModal.propTypes = {
 PageAccessoriesModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
+
   isGuestUser: PropTypes.bool.isRequired,
   isGuestUser: PropTypes.bool.isRequired,
   isSharedUser: PropTypes.bool.isRequired,
   isSharedUser: PropTypes.bool.isRequired,
-  isNotFoundPage: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func,
   onClose: PropTypes.func,
 };
 };

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

@@ -19,7 +19,7 @@ import { useCurrentPageId } from '~/stores/context';
 
 
 const PageAccessoriesModalControl = (props) => {
 const PageAccessoriesModalControl = (props) => {
   const {
   const {
-    t, pageAccessoriesContainer, isGuestUser, isSharedUser, isNotFoundPage,
+    t, pageAccessoriesContainer, isGuestUser, isSharedUser,
   } = props;
   } = props;
   const isLinkSharingDisabled = pageAccessoriesContainer.appContainer.config.disableLinkSharing;
   const isLinkSharingDisabled = pageAccessoriesContainer.appContainer.config.disableLinkSharing;
 
 
@@ -42,23 +42,22 @@ const PageAccessoriesModalControl = (props) => {
       {
       {
         name: 'pageHistory',
         name: 'pageHistory',
         Icon: <HistoryIcon />,
         Icon: <HistoryIcon />,
-        disabled: isGuestUser || isSharedUser || isNotFoundPage,
+        disabled: isGuestUser || isSharedUser,
         i18n: t('History'),
         i18n: t('History'),
       },
       },
       {
       {
         name: 'attachment',
         name: 'attachment',
         Icon: <AttachmentIcon />,
         Icon: <AttachmentIcon />,
-        disabled: isNotFoundPage,
         i18n: t('attachment_data'),
         i18n: t('attachment_data'),
       },
       },
       {
       {
         name: 'shareLink',
         name: 'shareLink',
         Icon: <ShareLinkIcon />,
         Icon: <ShareLinkIcon />,
-        disabled: isGuestUser || isSharedUser || isNotFoundPage || isLinkSharingDisabled,
+        disabled: isGuestUser || isSharedUser || isLinkSharingDisabled,
         i18n: t('share_links.share_link_management'),
         i18n: t('share_links.share_link_management'),
       },
       },
     ];
     ];
-  }, [t, isGuestUser, isSharedUser, isNotFoundPage, isLinkSharingDisabled]);
+  }, [t, isGuestUser, isSharedUser, isLinkSharingDisabled]);
 
 
   return (
   return (
     <div className="grw-page-accessories-control d-flex flex-nowrap align-items-center justify-content-end justify-content-lg-between">
     <div className="grw-page-accessories-control d-flex flex-nowrap align-items-center justify-content-end justify-content-lg-between">
@@ -66,7 +65,7 @@ const PageAccessoriesModalControl = (props) => {
 
 
         let tooltipMessage;
         let tooltipMessage;
         if (accessory.disabled) {
         if (accessory.disabled) {
-          tooltipMessage = isNotFoundPage ? t('not_found_page.page_not_exist') : t('Not available for guest');
+          tooltipMessage = t('Not available for guest');
           if (accessory.name === 'shareLink' && isLinkSharingDisabled) {
           if (accessory.name === 'shareLink' && isLinkSharingDisabled) {
             tooltipMessage = t('Link sharing is disabled');
             tooltipMessage = t('Link sharing is disabled');
           }
           }
@@ -111,7 +110,6 @@ PageAccessoriesModalControl.propTypes = {
 
 
   isGuestUser: PropTypes.bool.isRequired,
   isGuestUser: PropTypes.bool.isRequired,
   isSharedUser: PropTypes.bool.isRequired,
   isSharedUser: PropTypes.bool.isRequired,
-  isNotFoundPage: PropTypes.bool.isRequired,
 };
 };
 
 
 export default withTranslation()(PageAccessoriesModalControlWrapper);
 export default withTranslation()(PageAccessoriesModalControlWrapper);

+ 0 - 102
packages/app/src/components/PageList.jsx

@@ -1,102 +0,0 @@
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import Page from './PageList/Page';
-import { withUnstatedContainers } from './UnstatedUtils';
-
-import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
-
-import { useSWRxPageList } from '~/stores/page';
-
-import PaginationWrapper from './PaginationWrapper';
-
-
-const PageList = (props) => {
-  const { appContainer, pageContainer, t } = props;
-  const { path } = pageContainer.state;
-
-  const [activePage, setActivePage] = useState(1);
-
-  const { data: pagesListData, error: errors } = useSWRxPageList(path, activePage);
-
-  function setPageNumber(selectedPageNumber) {
-    setActivePage(selectedPageNumber);
-  }
-
-  if (errors != null) {
-    return (
-      <div className="my-5">
-        {/* eslint-disable-next-line react/no-array-index-key */}
-        {errors.map((error, index) => <div key={index} className="text-danger">{error.message}</div>)}
-      </div>
-    );
-  }
-
-  if (pagesListData == null) {
-    return (
-      <div className="wiki">
-        <div className="text-muted text-center">
-          <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
-        </div>
-      </div>
-    );
-  }
-
-  const liClasses = props.liClasses.join(' ');
-  const pageList = pagesListData.items.map(page => (
-    <li key={page._id} className={liClasses}>
-      <Page page={page} />
-    </li>
-  ));
-  if (pageList.length === 0) {
-    return (
-      <div className="mt-2">
-        {/* eslint-disable-next-line react/no-danger */}
-        <p>{t('custom_navigation.no_page_list')}</p>
-      </div>
-    );
-  }
-  if (appContainer.config.disableLinkSharing) {
-    return (
-      <div className="mt-2">
-        {/* eslint-disable-next-line react/no-danger */}
-        <p>{t('custom_navigation.link_sharing_is_disabled')}</p>
-      </div>
-    );
-  }
-
-  return (
-    <div className="page-list">
-      <ul className="page-list-ul page-list-ul-flat">
-        {pageList}
-      </ul>
-      <PaginationWrapper
-        activePage={activePage}
-        changePage={setPageNumber}
-        totalItemsCount={pagesListData.totalCount}
-        pagingLimit={pagesListData.limit}
-        align="center"
-      />
-    </div>
-  );
-};
-
-const PageListWrapper = withUnstatedContainers(PageList, [AppContainer, PageContainer]);
-
-const PageListTranslation = withTranslation()(PageListWrapper);
-
-
-PageList.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer),
-  pageContainer: PropTypes.instanceOf(PageContainer),
-
-  liClasses: PropTypes.arrayOf(PropTypes.string),
-};
-PageList.defaultProps = {
-  liClasses: ['mb-3'],
-};
-
-export default PageListTranslation;

+ 2 - 2
packages/app/src/components/PageList/BookmarkList.jsx

@@ -10,7 +10,7 @@ import { toastError } from '~/client/util/apiNotification';
 
 
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
 
 
-import Page from './Page';
+import PageListItemS from './PageListItemS';
 
 
 const logger = loggerFactory('growi:BookmarkList');
 const logger = loggerFactory('growi:BookmarkList');
 
 
@@ -56,7 +56,7 @@ const BookmarkList = (props) => {
    */
    */
   const generatePageList = pages.map(page => (
   const generatePageList = pages.map(page => (
     <li key={`my-bookmarks:${page._id}`} className="mt-4">
     <li key={`my-bookmarks:${page._id}`} className="mt-4">
-      <Page page={page.page} />
+      <PageListItemS page={page.page} />
     </li>
     </li>
   ));
   ));
 
 

+ 49 - 0
packages/app/src/components/PageList/PageList.tsx

@@ -0,0 +1,49 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { IPageHasId } from '~/interfaces/page';
+import { IPagingResult } from '~/interfaces/paging-result';
+
+import { PageListItemL } from './PageListItemL';
+
+
+type Props = {
+  pages: IPagingResult<IPageHasId>,
+}
+
+const PageList = (props: Props): JSX.Element => {
+  const { t } = useTranslation();
+  const { pages } = props;
+
+  if (pages == null) {
+    return (
+      <div className="wiki">
+        <div className="text-muted text-center">
+          <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+        </div>
+      </div>
+    );
+  }
+
+  const pageList = pages.items.map(page => (
+    <PageListItemL page={{ pageData: page }} />
+  ));
+
+  if (pageList.length === 0) {
+    return (
+      <div className="mt-2">
+        <p>{t('custom_navigation.no_page_list')}</p>
+      </div>
+    );
+  }
+
+  return (
+    <div className="page-list">
+      <ul className="page-list-ul page-list-ul-flat">
+        {pageList}
+      </ul>
+    </div>
+  );
+};
+
+export default PageList;

+ 28 - 27
packages/app/src/components/Page/PageListItem.tsx → packages/app/src/components/PageList/PageListItemL.tsx

@@ -5,28 +5,29 @@ import Clamp from 'react-multiline-clamp';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 import { pagePathUtils, DevidedPagePath } from '@growi/core';
 import { pagePathUtils, DevidedPagePath } from '@growi/core';
 import { useIsDeviceSmallerThanLg } from '~/stores/ui';
 import { useIsDeviceSmallerThanLg } from '~/stores/ui';
+import { IPageWithMeta } from '~/interfaces/page';
+import { IPageSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
 
 
-import { IPageSearchResultData } from '../../interfaces/search';
 import PageItemControl from '../Common/Dropdown/PageItemControl';
 import PageItemControl from '../Common/Dropdown/PageItemControl';
 
 
 const { isTopPage } = pagePathUtils;
 const { isTopPage } = pagePathUtils;
 
 
 type Props = {
 type Props = {
-  page: IPageSearchResultData,
-  isSelected: boolean, // is item selected(focused)
-  isChecked: boolean, // is checkbox of item checked
-  isEnableActions: boolean,
+  page: IPageWithMeta | IPageWithMeta<IPageSearchMeta>,
+  isSelected?: boolean, // is item selected(focused)
+  isChecked?: boolean, // is checkbox of item checked
+  isEnableActions?: boolean,
   shortBody?: string
   shortBody?: string
   showPageUpdatedTime?: boolean, // whether to show page's updated time at the top-right corner of item
   showPageUpdatedTime?: boolean, // whether to show page's updated time at the top-right corner of item
   onClickCheckbox?: (pageId: string) => void,
   onClickCheckbox?: (pageId: string) => void,
-  onClickSearchResultItem?: (pageId: string) => void,
+  onClickItem?: (pageId: string) => void,
   onClickDeleteButton?: (pageId: string) => void,
   onClickDeleteButton?: (pageId: string) => void,
 }
 }
 
 
-const PageListItem: FC<Props> = memo((props:Props) => {
+export const PageListItemL: FC<Props> = memo((props:Props) => {
   const {
   const {
     // todo: refactoring variable name to clear what changed
     // todo: refactoring variable name to clear what changed
-    page: { pageData, pageMeta }, isSelected, onClickSearchResultItem, onClickCheckbox, isChecked, isEnableActions, shortBody,
+    page: { pageData, pageMeta }, isSelected, onClickItem, onClickCheckbox, isChecked, isEnableActions, shortBody,
     showPageUpdatedTime,
     showPageUpdatedTime,
   } = props;
   } = props;
 
 
@@ -34,19 +35,21 @@ const PageListItem: FC<Props> = memo((props:Props) => {
 
 
   const pagePath: DevidedPagePath = new DevidedPagePath(pageData.path, true);
   const pagePath: DevidedPagePath = new DevidedPagePath(pageData.path, true);
 
 
+  const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null;
+
   const pageTitle = (
   const pageTitle = (
     <PagePathLabel
     <PagePathLabel
-      path={pageMeta.elasticSearchResult?.highlightedPath || pageData.path}
+      path={elasticSearchResult?.highlightedPath || pageData.path}
       isLatterOnly
       isLatterOnly
-      isPathIncludedHtml={pageMeta.elasticSearchResult?.isHtmlInPath}
+      isPathIncludedHtml={elasticSearchResult?.isHtmlInPath}
     >
     >
     </PagePathLabel>
     </PagePathLabel>
   );
   );
   const pagePathElem = (
   const pagePathElem = (
     <PagePathLabel
     <PagePathLabel
-      path={pageMeta.elasticSearchResult?.highlightedPath || pageData.path}
+      path={elasticSearchResult?.highlightedPath || pageData.path}
       isFormerOnly
       isFormerOnly
-      isPathIncludedHtml={pageMeta.elasticSearchResult?.isHtmlInPath}
+      isPathIncludedHtml={elasticSearchResult?.isHtmlInPath}
     />
     />
   );
   );
 
 
@@ -57,27 +60,27 @@ const PageListItem: FC<Props> = memo((props:Props) => {
       return;
       return;
     }
     }
 
 
-    if (onClickSearchResultItem != null) {
-      onClickSearchResultItem(pageData._id);
+    if (onClickItem != null) {
+      onClickItem(pageData._id);
     }
     }
-  }, [isDeviceSmallerThanLg, onClickSearchResultItem, pageData._id]);
+  }, [isDeviceSmallerThanLg, onClickItem, pageData._id]);
 
 
   const styleListGroupItem = (!isDeviceSmallerThanLg && onClickCheckbox != null) ? 'list-group-item-action' : '';
   const styleListGroupItem = (!isDeviceSmallerThanLg && onClickCheckbox != null) ? 'list-group-item-action' : '';
-  // background color of list item changes when class "active" exists under 'grw-search-result-item'
+  // background color of list item changes when class "active" exists under 'list-group-item'
   const styleActive = !isDeviceSmallerThanLg && isSelected ? 'active' : '';
   const styleActive = !isDeviceSmallerThanLg && isSelected ? 'active' : '';
   const styleBorder = onClickCheckbox != null ? 'border-bottom' : 'list-group-item p-0';
   const styleBorder = onClickCheckbox != null ? 'border-bottom' : 'list-group-item p-0';
 
 
   return (
   return (
     <li
     <li
       key={pageData._id}
       key={pageData._id}
-      className={`w-100 grw-search-result-item search-result-list ${styleListGroupItem} ${styleActive} ${styleBorder}}`
+      className={`list-group-item p-0 ${styleListGroupItem} ${styleActive} ${styleBorder}}`
       }
       }
     >
     >
       <div
       <div
-        className="h-100 text-break"
+        className="text-break"
         onClick={clickHandler}
         onClick={clickHandler}
       >
       >
-        <div className="d-flex h-100">
+        <div className="d-flex">
           {/* checkbox */}
           {/* checkbox */}
           {onClickCheckbox != null && (
           {onClickCheckbox != null && (
             <div className="form-check d-flex align-items-center justify-content-center px-md-2 pl-3 pr-2 search-item-checkbox">
             <div className="form-check d-flex align-items-center justify-content-center px-md-2 pl-3 pr-2 search-item-checkbox">
@@ -90,7 +93,7 @@ const PageListItem: FC<Props> = memo((props:Props) => {
               />
               />
             </div>
             </div>
           )}
           )}
-          <div className="search-item-text p-md-3 pl-2 py-3 pr-3 flex-grow-1">
+          <div className="flex-grow-1 p-md-3 pl-2 py-3 pr-3">
             {/* page path */}
             {/* page path */}
             <h6 className="mb-1 py-1 d-flex">
             <h6 className="mb-1 py-1 d-flex">
               <a className="d-inline-block" href={pagePath.isRoot ? pagePath.latter : pagePath.former}>
               <a className="d-inline-block" href={pagePath.isRoot ? pagePath.latter : pagePath.former}>
@@ -112,8 +115,8 @@ const PageListItem: FC<Props> = memo((props:Props) => {
               </Clamp>
               </Clamp>
 
 
               {/* page meta */}
               {/* page meta */}
-              <div className="d-none d-md-flex item-meta py-0 px-1">
-                <PageListMeta page={pageData} bookmarkCount={pageMeta.bookmarkCount} shouldSpaceOutIcon />
+              <div className="d-none d-md-flex py-0 px-1">
+                <PageListMeta page={pageData} bookmarkCount={pageMeta?.bookmarkCount} shouldSpaceOutIcon />
               </div>
               </div>
               {/* doropdown icon includes page control buttons */}
               {/* doropdown icon includes page control buttons */}
               <div className="item-control ml-auto">
               <div className="item-control ml-auto">
@@ -126,11 +129,11 @@ const PageListItem: FC<Props> = memo((props:Props) => {
                 />
                 />
               </div>
               </div>
             </div>
             </div>
-            <div className="grw-search-result-list-snippet py-1">
+            <div className="page-list-snippet py-1">
               <Clamp lines={2}>
               <Clamp lines={2}>
                 {
                 {
-                  pageMeta.elasticSearchResult != null && pageMeta.elasticSearchResult?.snippet.length !== 0 ? (
-                    <div dangerouslySetInnerHTML={{ __html: pageMeta.elasticSearchResult.snippet }}></div>
+                  elasticSearchResult != null && elasticSearchResult?.snippet.length !== 0 ? (
+                    <div dangerouslySetInnerHTML={{ __html: elasticSearchResult.snippet }}></div>
                   ) : (
                   ) : (
                     <div>{ shortBody != null ? shortBody : 'Loading ...' }</div> // TODO: improve indicator
                     <div>{ shortBody != null ? shortBody : 'Loading ...' }</div> // TODO: improve indicator
                   )
                   )
@@ -144,5 +147,3 @@ const PageListItem: FC<Props> = memo((props:Props) => {
     </li>
     </li>
   );
   );
 });
 });
-
-export default PageListItem;

+ 3 - 3
packages/app/src/components/PageList/Page.jsx → packages/app/src/components/PageList/PageListItemS.jsx

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 
 
 
 
-export default class Page extends React.Component {
+export default class PageListItemS extends React.Component {
 
 
   render() {
   render() {
     const {
     const {
@@ -27,11 +27,11 @@ export default class Page extends React.Component {
 
 
 }
 }
 
 
-Page.propTypes = {
+PageListItemS.propTypes = {
   page: PropTypes.object.isRequired,
   page: PropTypes.object.isRequired,
   noLink: PropTypes.bool,
   noLink: PropTypes.bool,
 };
 };
 
 
-Page.defaultProps = {
+PageListItemS.defaultProps = {
   noLink: false,
   noLink: false,
 };
 };

+ 2 - 2
packages/app/src/components/RecentCreated/RecentCreated.jsx

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 
 
-import Page from '../PageList/Page';
+import PageListItemS from '../PageList/PageListItemS';
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
 
 
 class RecentCreated extends React.Component {
 class RecentCreated extends React.Component {
@@ -57,7 +57,7 @@ class RecentCreated extends React.Component {
   generatePageList(pages) {
   generatePageList(pages) {
     return pages.map(page => (
     return pages.map(page => (
       <li key={`recent-created:list-view:${page._id}`} className="mt-4">
       <li key={`recent-created:list-view:${page._id}`} className="mt-4">
-        <Page page={page} />
+        <PageListItemS page={page} />
       </li>
       </li>
     ));
     ));
   }
   }

+ 3 - 2
packages/app/src/components/SearchForm.tsx

@@ -4,8 +4,9 @@ import React, {
 } from 'react';
 } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import { IPageSearchResultData } from '~/interfaces/search';
 import { IFocusable } from '~/client/interfaces/focusable';
 import { IFocusable } from '~/client/interfaces/focusable';
+import { IPageWithMeta } from '~/interfaces/page';
+import { IPageSearchMeta } from '~/interfaces/search';
 
 
 import SearchTypeahead from './SearchTypeahead';
 import SearchTypeahead from './SearchTypeahead';
 
 
@@ -84,7 +85,7 @@ type Props = {
 
 
   dropup?: boolean,
   dropup?: boolean,
   keyword?: string,
   keyword?: string,
-  onChange?: (data: IPageSearchResultData[]) => void,
+  onChange?: (data: IPageWithMeta<IPageSearchMeta>[]) => void,
   onBlur?: () => void,
   onBlur?: () => void,
   onFocus?: () => void,
   onFocus?: () => void,
   onSubmit?: (input: string) => void,
   onSubmit?: (input: string) => void,

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

@@ -327,7 +327,7 @@ class SearchPage extends React.Component {
         shortBodiesMap={this.state.shortBodiesMap}
         shortBodiesMap={this.state.shortBodiesMap}
         activePage={this.state.activePage}
         activePage={this.state.activePage}
         pagingLimit={this.state.pagingLimit}
         pagingLimit={this.state.pagingLimit}
-        onClickSearchResultItem={this.selectPage}
+        onClickItem={this.selectPage}
         onClickCheckbox={this.toggleCheckBox}
         onClickCheckbox={this.toggleCheckBox}
         onPagingNumberChanged={this.onPagingNumberChanged}
         onPagingNumberChanged={this.onPagingNumberChanged}
         onClickDeleteButton={this.deleteSinglePageButtonHandler}
         onClickDeleteButton={this.deleteSinglePageButtonHandler}

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

@@ -34,7 +34,7 @@ const SearchPageLayout: FC<Props> = (props: Props) => {
   return (
   return (
     <div className="content-main">
     <div className="content-main">
       <div className="search-result d-flex" id="search-result">
       <div className="search-result d-flex" id="search-result">
-        <div className="mw-0 flex-grow-1 flex-basis-0 page-list border boder-gray search-result-list" id="search-result-list">
+        <div className="mw-0 flex-grow-1 flex-basis-0 border boder-gray search-result-list" id="search-result-list">
 
 
           <SearchControl></SearchControl>
           <SearchControl></SearchControl>
           <div className="search-result-list-scroll">
           <div className="search-result-list-scroll">
@@ -62,8 +62,8 @@ const SearchPageLayout: FC<Props> = (props: Props) => {
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="page-list">
-              <ul className="page-list-ul page-list-ul-flat px-md-4 nav nav-pills"><SearchResultList></SearchResultList></ul>
+            <div className="page-list px-md-4">
+              <SearchResultList></SearchResultList>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>

+ 3 - 5
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -1,18 +1,16 @@
 import React, { FC } from 'react';
 import React, { FC } from 'react';
 
 
-import { IPageSearchResultData } from '../../interfaces/search';
+import { IPageWithMeta } from '~/interfaces/page';
+import { IPageSearchMeta } from '~/interfaces/search';
 
 
 import RevisionLoader from '../Page/RevisionLoader';
 import RevisionLoader from '../Page/RevisionLoader';
 import AppContainer from '../../client/services/AppContainer';
 import AppContainer from '../../client/services/AppContainer';
 import SearchResultContentSubNavigation from './SearchResultContentSubNavigation';
 import SearchResultContentSubNavigation from './SearchResultContentSubNavigation';
 
 
-// TODO : set focusedPage type to ?IPageSearchResultData once #80214 is merged
-// PR: https://github.com/weseek/growi/pull/4649
-
 type Props ={
 type Props ={
   appContainer: AppContainer,
   appContainer: AppContainer,
   searchingKeyword:string,
   searchingKeyword:string,
-  focusedSearchResultData : IPageSearchResultData,
+  focusedSearchResultData : IPageWithMeta<IPageSearchMeta>,
 }
 }
 
 
 
 

+ 11 - 9
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -1,20 +1,22 @@
 import React, { FC } from 'react';
 import React, { FC } from 'react';
-import PageListItem from '../Page/PageListItem';
+import { IPageWithMeta } from '~/interfaces/page';
+import { IPageSearchMeta } from '~/interfaces/search';
+
+import { PageListItemL } from '../PageList/PageListItemL';
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
-import { IPageSearchResultData } from '../../interfaces/search';
 
 
 
 
 type Props = {
 type Props = {
-  pages: IPageSearchResultData[],
+  pages: IPageWithMeta<IPageSearchMeta>[],
   selectedPagesIdList: Set<string>
   selectedPagesIdList: Set<string>
   isEnableActions: boolean,
   isEnableActions: boolean,
   searchResultCount?: number,
   searchResultCount?: number,
   activePage?: number,
   activePage?: number,
   pagingLimit?: number,
   pagingLimit?: number,
   shortBodiesMap?: Record<string, string>
   shortBodiesMap?: Record<string, string>
-  focusedSearchResultData?: IPageSearchResultData,
+  focusedSearchResultData?: IPageWithMeta<IPageSearchMeta>,
   onPagingNumberChanged?: (activePage: number) => void,
   onPagingNumberChanged?: (activePage: number) => void,
-  onClickSearchResultItem?: (pageId: string) => void,
+  onClickItem?: (pageId: string) => void,
   onClickCheckbox?: (pageId: string) => void,
   onClickCheckbox?: (pageId: string) => void,
   onClickInvoked?: (pageId: string) => void,
   onClickInvoked?: (pageId: string) => void,
   onClickDeleteButton?: (pageId: string) => void,
   onClickDeleteButton?: (pageId: string) => void,
@@ -27,17 +29,17 @@ const SearchResultList: FC<Props> = (props:Props) => {
 
 
   const focusedPageId = (focusedSearchResultData != null && focusedSearchResultData.pageData != null) ? focusedSearchResultData.pageData._id : '';
   const focusedPageId = (focusedSearchResultData != null && focusedSearchResultData.pageData != null) ? focusedSearchResultData.pageData._id : '';
   return (
   return (
-    <>
+    <ul className="page-list-ul list-group list-group-flush">
       {Array.isArray(props.pages) && props.pages.map((page) => {
       {Array.isArray(props.pages) && props.pages.map((page) => {
         const isChecked = selectedPagesIdList.has(page.pageData._id);
         const isChecked = selectedPagesIdList.has(page.pageData._id);
 
 
         return (
         return (
-          <PageListItem
+          <PageListItemL
             key={page.pageData._id}
             key={page.pageData._id}
             page={page}
             page={page}
             isEnableActions={isEnableActions}
             isEnableActions={isEnableActions}
             shortBody={shortBodiesMap?.[page.pageData._id]}
             shortBody={shortBodiesMap?.[page.pageData._id]}
-            onClickSearchResultItem={props.onClickSearchResultItem}
+            onClickItem={props.onClickItem}
             onClickCheckbox={props.onClickCheckbox}
             onClickCheckbox={props.onClickCheckbox}
             isChecked={isChecked}
             isChecked={isChecked}
             isSelected={page.pageData._id === focusedPageId || false}
             isSelected={page.pageData._id === focusedPageId || false}
@@ -56,7 +58,7 @@ const SearchResultList: FC<Props> = (props:Props) => {
         </div>
         </div>
       )}
       )}
 
 
-    </>
+    </ul>
   );
   );
 
 
 };
 };

+ 5 - 4
packages/app/src/components/SearchTypeahead.tsx

@@ -10,7 +10,8 @@ import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 import { IFocusable } from '~/client/interfaces/focusable';
 import { IFocusable } from '~/client/interfaces/focusable';
 import { TypeaheadProps } from '~/client/interfaces/react-bootstrap-typeahead';
 import { TypeaheadProps } from '~/client/interfaces/react-bootstrap-typeahead';
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
-import { IPageSearchResultData, IFormattedSearchResult } from '~/interfaces/search';
+import { IFormattedSearchResult, IPageSearchMeta } from '~/interfaces/search';
+import { IPageWithMeta } from '~/interfaces/page';
 
 
 
 
 type ResetFormButtonProps = {
 type ResetFormButtonProps = {
@@ -33,7 +34,7 @@ const ResetFormButton: FC<ResetFormButtonProps> = (props: ResetFormButtonProps)
 
 
 
 
 type Props = TypeaheadProps & {
 type Props = TypeaheadProps & {
-  onSearchSuccess?: (res: IPageSearchResultData[]) => void,
+  onSearchSuccess?: (res: IPageWithMeta<IPageSearchMeta>[]) => void,
   onSearchError?: (err: Error) => void,
   onSearchError?: (err: Error) => void,
   onSubmit?: (input: string) => void,
   onSubmit?: (input: string) => void,
   inputName?: string,
   inputName?: string,
@@ -60,7 +61,7 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
 
 
   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
   const [input, setInput] = useState(props.keywordOnInit!);
   const [input, setInput] = useState(props.keywordOnInit!);
-  const [pages, setPages] = useState<IPageSearchResultData[]>();
+  const [pages, setPages] = useState<IPageWithMeta<IPageSearchMeta>[]>();
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   const [searchError, setSearchError] = useState<Error | null>(null);
   const [searchError, setSearchError] = useState<Error | null>(null);
   const [isLoading, setLoading] = useState(false);
   const [isLoading, setLoading] = useState(false);
@@ -187,7 +188,7 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
     inputProps.name = props.inputName;
     inputProps.name = props.inputName;
   }
   }
 
 
-  const renderMenuItemChildren = (option: IPageSearchResultData) => {
+  const renderMenuItemChildren = (option: IPageWithMeta<IPageSearchMeta>) => {
     const { pageData } = option;
     const { pageData } = option;
     return (
     return (
       <span>
       <span>

+ 2 - 2
packages/app/src/components/TrashPageList.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import PageListIcon from './Icons/PageListIcon';
 import PageListIcon from './Icons/PageListIcon';
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
-import PageList from './PageList';
+import DescendantsPageList from './DescendantsPageList';
 
 
 
 
 const TrashPageList = (props) => {
 const TrashPageList = (props) => {
@@ -13,7 +13,7 @@ const TrashPageList = (props) => {
     return {
     return {
       pagelist: {
       pagelist: {
         Icon: PageListIcon,
         Icon: PageListIcon,
-        Content: PageList,
+        Content: DescendantsPageList,
         i18n: t('page_list'),
         i18n: t('page_list'),
         index: 0,
         index: 0,
       },
       },

+ 15 - 0
packages/app/src/interfaces/page.ts

@@ -34,3 +34,18 @@ export type IPage = {
 export type IPageHasId = IPage & HasObjectId;
 export type IPageHasId = IPage & HasObjectId;
 
 
 export type IPageForItem = Partial<IPageHasId & {isTarget?: boolean}>;
 export type IPageForItem = Partial<IPageHasId & {isTarget?: boolean}>;
+
+export type IPageInfo = {
+  bookmarkCount: number,
+  sumOfLikers: number,
+  likerIds: string[],
+  sumOfSeenUsers: number,
+  seenUserIds: string[],
+  isSeen?: boolean,
+  isLiked?: boolean,
+}
+
+export type IPageWithMeta<M = Record<string, unknown>> = {
+  pageData: IPageHasId,
+  pageMeta?: Partial<IPageInfo> & M,
+};

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

@@ -1,4 +1,4 @@
-import { IPageHasId } from './page';
+import { IPageWithMeta } from './page';
 
 
 export enum CheckboxType {
 export enum CheckboxType {
   NONE_CHECKED = 'noneChecked',
   NONE_CHECKED = 'noneChecked',
@@ -6,20 +6,20 @@ export enum CheckboxType {
   ALL_CHECKED = 'allChecked',
   ALL_CHECKED = 'allChecked',
 }
 }
 
 
-export type IPageSearchResultData = {
-  pageData: IPageHasId;
-  pageMeta: {
-    bookmarkCount?: number;
-    elasticSearchResult?: {
-      snippet: string;
-      highlightedPath: string;
-      isHtmlInPath: boolean;
-    };
+export type IPageSearchMeta = {
+  elasticSearchResult?: {
+    snippet: string;
+    highlightedPath: string;
+    isHtmlInPath: boolean;
   };
   };
+}
+
+export const isIPageSearchMeta = (meta: any): meta is IPageSearchMeta => {
+  return !!(meta as IPageSearchMeta)?.elasticSearchResult;
 };
 };
 
 
 export type IFormattedSearchResult = {
 export type IFormattedSearchResult = {
-  data: IPageSearchResultData[]
+  data: IPageWithMeta<IPageSearchMeta>[]
 
 
   totalCount: number
   totalCount: number
 
 

+ 0 - 7
packages/app/src/server/routes/page.js

@@ -302,10 +302,6 @@ module.exports = function(crowi, app) {
     renderVars.shortBodyMap = shortBodyMap;
     renderVars.shortBodyMap = shortBodyMap;
   }
   }
 
 
-  function addRenderVarsWhenNotCreatableOrForbidden(renderVars) {
-    renderVars.isAlertHidden = true;
-  }
-
   function replacePlaceholdersOfTemplate(template, req) {
   function replacePlaceholdersOfTemplate(template, req) {
     if (req.user == null) {
     if (req.user == null) {
       return '';
       return '';
@@ -329,11 +325,9 @@ module.exports = function(crowi, app) {
     const renderVars = { path };
     const renderVars = { path };
 
 
     if (!isCreatablePage(path)) {
     if (!isCreatablePage(path)) {
-      addRenderVarsWhenNotCreatableOrForbidden(renderVars);
       view = 'layout-growi/not_creatable';
       view = 'layout-growi/not_creatable';
     }
     }
     else if (req.isForbidden) {
     else if (req.isForbidden) {
-      addRenderVarsWhenNotCreatableOrForbidden(renderVars);
       view = 'layout-growi/forbidden';
       view = 'layout-growi/forbidden';
     }
     }
     else {
     else {
@@ -527,7 +521,6 @@ module.exports = function(crowi, app) {
       return res.render('layout-growi/not_found_shared_page');
       return res.render('layout-growi/not_found_shared_page');
     }
     }
     if (crowi.configManager.getConfig('crowi', 'security:disableLinkSharing')) {
     if (crowi.configManager.getConfig('crowi', 'security:disableLinkSharing')) {
-      addRenderVarsWhenNotCreatableOrForbidden(renderVars);
       return res.render('layout-growi/forbidden');
       return res.render('layout-growi/forbidden');
     }
     }
 
 

+ 1 - 0
packages/app/src/server/views/layout-growi/not_found.html

@@ -10,6 +10,7 @@
   </div>
   </div>
   <div class="grw-container-convertible">
   <div class="grw-container-convertible">
     {% include '../widget/page_alerts.html' %}
     {% include '../widget/page_alerts.html' %}
+    <div id="not-found-alert"></div>
   </div>
   </div>
 {% endblock %}
 {% endblock %}
 
 

+ 0 - 3
packages/app/src/server/views/widget/page_alerts.html

@@ -76,8 +76,5 @@
     {% if isTrashPage() %}
     {% if isTrashPage() %}
       <div id="trash-page-alert"></div>
       <div id="trash-page-alert"></div>
     {% endif %}
     {% endif %}
-    {% if page == null and !isAlertHidden %}
-      <div id="not-found-alert"></div>
-    {% endif %}
   </div>
   </div>
 </div>
 </div>

+ 22 - 0
packages/app/src/styles/_page_list.scss

@@ -54,6 +54,28 @@ body .page-list {
       padding-left: 0;
       padding-left: 0;
     }
     }
   }
   }
+
+  // List group
+  .list-group {
+    .list-group-item {
+      min-height: 136px;
+
+      .page-list-meta {
+        .meta-icon {
+          width: 14px;
+          height: 14px;
+          margin-right: 14px;
+        }
+        .footstamp-icon {
+          margin-right: 2px;
+        }
+      }
+
+      &.list-group-item-action {
+        border-left: solid 3px transparent;
+      }
+    }
+  }
 }
 }
 
 
 .popular-page-high {
 .popular-page-high {

+ 4 - 46
packages/app/src/styles/_search.scss

@@ -142,9 +142,6 @@
     padding-bottom: unset;
     padding-bottom: unset;
   }
   }
 
 
-  .search-control {
-    padding: 5px 0;
-  }
   // To fix the sort options position
   // To fix the sort options position
   .search-sort-option-btn {
   .search-sort-option-btn {
     min-width: 12rem;
     min-width: 12rem;
@@ -155,10 +152,6 @@
     }
     }
   }
   }
   .search-result-list {
   .search-result-list {
-    position: sticky;
-    top: 0px;
-    z-index: 10; // to avoid dropdown menu in this class to be placed behind elements displayed on the right pane
-
     .search-result-list-scroll {
     .search-result-list-scroll {
       // subtract the height of GrowiNavbar + (SearchControl component + other factors)
       // subtract the height of GrowiNavbar + (SearchControl component + other factors)
       height: calc(100vh - (($grw-navbar-height + $grw-navbar-border-width) + 110px));
       height: calc(100vh - (($grw-navbar-height + $grw-navbar-border-width) + 110px));
@@ -169,41 +162,6 @@
       }
       }
     }
     }
 
 
-    .nav.nav-pills {
-      > .grw-search-result-item {
-        min-height: 136px;
-        &.active {
-          border-left: solid 3px transparent;
-          .search-item-checkbox {
-            // subtract 3px from padding left applied by .search-item-checkbox
-            // as 3px of border-left is added above
-            padding-left: 4px !important;
-          }
-        }
-        > a {
-          word-break: break-all;
-
-          &:hover {
-            color: inherit;
-            text-decoration: none;
-          }
-          > * {
-            margin-right: 3px;
-          }
-        }
-        .page-list-meta {
-          .meta-icon {
-            width: 14px;
-            height: 14px;
-            margin-right: 14px;
-          }
-          .footstamp-icon {
-            margin-right: 2px;
-          }
-        }
-      }
-    }
-
     .search-result-meta {
     .search-result-meta {
       font-weight: bold;
       font-weight: bold;
     }
     }
@@ -216,14 +174,14 @@
       margin: 0 10px 0 0;
       margin: 0 10px 0 0;
       vertical-align: middle;
       vertical-align: middle;
     }
     }
-  }
-  .search-item-text {
-    .item-meta {
+    // not show top label in search result list
+    .page-list-meta {
       .top-label {
       .top-label {
-        display: none; // not show top label in search result list
+        display: none;
       }
       }
     }
     }
   }
   }
+
   .search-result-content {
   .search-result-content {
     .search-result-content-nav {
     .search-result-content-nav {
       min-height: $grw-subnav-search-preview-min-height;
       min-height: $grw-subnav-search-preview-min-height;

+ 13 - 18
packages/app/src/styles/theme/_apply-colors-dark.scss

@@ -218,6 +218,19 @@ ul.pagination {
       }
       }
     }
     }
   }
   }
+
+  // List group
+  .list-group-item {
+    &.active {
+      background-color: lighten($bgcolor-global, 9%) !important;
+    }
+    .list-group-item-action:hover {
+      background-color: $bgcolor-list-hover;
+    }
+    .page-list-snippet {
+      color: theme-color('light');
+    }
+  }
 }
 }
 
 
 /*
 /*
@@ -442,21 +455,3 @@ ul.pagination {
 .grw-modal-head {
 .grw-modal-head {
   border-color: $border-color-global;
   border-color: $border-color-global;
 }
 }
-
-/*
- * search page
- */
-.on-search {
-  .grw-search-result-item {
-    &.active {
-      background-color: lighten($bgcolor-global, 9%) !important;
-    }
-  }
-  .list-group-item-action:hover {
-    background-color: $bgcolor-list-hover;
-  }
-
-  .grw-search-result-list-snippet {
-    color: theme-color('light');
-  }
-}

+ 37 - 29
packages/app/src/styles/theme/_apply-colors.scss

@@ -17,10 +17,10 @@ $bordercolor-nav-tabs-active: $bordercolor-nav-tabs $bordercolor-nav-tabs $bgcol
 $color-seen-user: #549c79 !default;
 $color-seen-user: #549c79 !default;
 $color-btn-reload-in-sidebar: $gray-500;
 $color-btn-reload-in-sidebar: $gray-500;
 $bgcolor-keyword-highlighted: $grw-marker-yellow !default;
 $bgcolor-keyword-highlighted: $grw-marker-yellow !default;
-$bgcolor-search-item-active: lighten($primary, 76%) !default;
-$color-search-item-pagelist-meta: $gray-500 !default;
+$bgcolor-page-list-group-item-active: lighten($primary, 76%) !default;
+$color-page-list-group-item-meta: $gray-500 !default;
 $color-search-page-list-title: $color-global !default;
 $color-search-page-list-title: $color-global !default;
-$color-search-page-list-snippet: $gray-600 !default;
+$color-page-list-snippet: $gray-600 !default;
 $bgcolor-subnav: darken($bgcolor-global, 3%) !default;
 $bgcolor-subnav: darken($bgcolor-global, 3%) !default;
 
 
 // override bootstrap variables
 // override bootstrap variables
@@ -510,6 +510,34 @@ ul.pagination {
   }
   }
 }
 }
 
 
+/*
+ * GROWI page-list
+ */
+.page-list {
+  // List group
+  .list-group {
+    .list-group-item {
+      .page-list-meta {
+        color: $color-page-list-group-item-meta;
+        svg {
+          fill: $color-page-list-group-item-meta;
+        }
+      }
+
+      .page-list-snippet {
+        color: $color-page-list-snippet;
+      }
+
+      &.list-group-item-action {
+        &.active {
+          background-color: $bgcolor-page-list-group-item-active;
+          border-left-color: $primary;
+        }
+      }
+    }
+  }
+}
+
 /*
 /*
  * GROWI on-edit
  * GROWI on-edit
  */
  */
@@ -616,33 +644,13 @@ body.pathname-sidebar {
   .grw-search-page-nav {
   .grw-search-page-nav {
     background-color: $bgcolor-subnav;
     background-color: $bgcolor-subnav;
   }
   }
-  .search-result-list {
-    .search-control {
-      background-color: $bgcolor-global;
-    }
-    .page-list {
-      .highlighted-keyword {
-        background-color: $bgcolor-keyword-highlighted;
-      }
-      .page-list-ul {
-        .grw-search-result-item {
-          &.active {
-            background-color: $bgcolor-search-item-active;
-            border-color: $primary;
-          }
-        }
-      }
-      .page-list-meta {
-        color: $color-search-item-pagelist-meta;
-        svg {
-          fill: $color-search-item-pagelist-meta;
-        }
-      }
-    }
+  .search-control {
+    background-color: $bgcolor-global;
   }
   }
-
-  .grw-search-result-list-snippet {
-    color: $color-search-page-list-snippet;
+  .page-list {
+    .highlighted-keyword {
+      background-color: $bgcolor-keyword-highlighted;
+    }
   }
   }
 }
 }