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

Merge branch 'feat/77545-81664-display-filter-option' into feat/77545-81845-filtering-search-result

# Conflicts:
#	packages/app/src/components/SearchPage.jsx
#	packages/app/src/components/SearchPage/SearchControl.tsx
#	packages/app/src/components/SearchPage/SearchOptionModal.tsx
SULLEY\ryo-h 4 лет назад
Родитель
Сommit
927358a736
24 измененных файлов с 169 добавлено и 141 удалено
  1. 1 1
      packages/app/resource/locales/en_US/translation.json
  2. 2 2
      packages/app/resource/locales/ja_JP/translation.json
  3. 1 1
      packages/app/resource/locales/zh_CN/translation.json
  4. 1 1
      packages/app/src/components/PageList/Page.jsx
  5. 13 13
      packages/app/src/components/SearchPage.jsx
  6. 20 20
      packages/app/src/components/SearchPage/SearchControl.tsx
  7. 8 8
      packages/app/src/components/SearchPage/SearchOptionModal.tsx
  8. 25 31
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  9. 2 5
      packages/app/src/components/SearchPage/SearchResultListItem.tsx
  10. 1 1
      packages/app/src/components/SearchTypeahead.jsx
  11. 1 3
      packages/app/src/components/Sidebar/RecentChanges.jsx
  12. 1 1
      packages/app/src/components/User/SeenUserInfo.jsx
  13. 1 1
      packages/app/src/interfaces/search.ts
  14. 2 0
      packages/app/src/server/routes/search.js
  15. 1 2
      packages/app/src/server/service/search.js
  16. 8 0
      packages/app/src/styles/_search.scss
  17. 8 1
      packages/app/src/styles/theme/_apply-colors.scss
  18. 2 2
      packages/core/src/models/devided-page-path.js
  19. 1 1
      packages/plugin-lsx/src/client/js/components/LsxPageList/PagePathWrapper.jsx
  20. 12 4
      packages/ui/src/components/PagePath/PageListMeta.jsx
  21. 0 40
      packages/ui/src/components/PagePath/PagePathLabel.jsx
  22. 56 0
      packages/ui/src/components/PagePath/PagePathLabel.tsx
  23. 1 3
      packages/ui/src/components/SearchPage/FootstampIcon.jsx
  24. 1 0
      packages/ui/src/index.ts

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

@@ -577,7 +577,7 @@
     "delete_completely": "Delete completely",
     "include_certain_path" : "Include {{pathToInclude}} path ",
     "delete_all_selected_page" : "Delete All",
-    "narrow_donw" : "Narrow down"
+    "search_again" : "Search again"
   },
   "security_setting": {
     "Guest Users Access": "Guest users access",

+ 2 - 2
packages/app/resource/locales/ja_JP/translation.json

@@ -64,7 +64,7 @@
   "Include Attachment File": "添付ファイルも含める",
   "Include Comment": "コメントも含める",
   "Include Subordinated Page": "配下ページも含める",
-  "Include Subordinated Target Page": "{{target}} 下含む",
+  "Include Subordinated Target Page": "{{target}} 下含む",
   "All Subordinated Page": "全ての配下ページ",
   "Specify Hierarchy": "階層の深さを指定",
   "Submitted the request to create the archive": "アーカイブ作成のリクエストを正常に送信しました",
@@ -577,7 +577,7 @@
     "delete_completely": "完全に削除する",
     "include_certain_path": "{{pathToInclude}}下を含む ",
     "delete_all_selected_page" : "一括削除",
-    "narrow_donw" : "絞り込む"
+    "search_again" : "再検索"
   },
   "security_setting": {
     "Guest Users Access": "ゲストユーザーのアクセス",

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

@@ -850,7 +850,7 @@
 		"delete_completely": "完全删除",
     "include_certain_path": "包含 {{pathToInclude}} 路径 ",
     "delete_all_selected_page": "删除所有",
-    "narrow_donw" : "细化您的搜索"
+    "search_again" : "再次搜索"
 	},
 	"to_cloud_settings": "進入 GROWI.cloud 的管理界面",
 	"login": {

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

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

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

@@ -30,13 +30,13 @@ class SearchPage extends React.Component {
       searchedKeyword: '',
       searchResults: [],
       searchResultMeta: {},
-      focusedSearchResultData: {},
+      focusedSearchResultData: null,
       selectedPages: new Set(),
       searchResultCount: 0,
       activePage: 1,
       pagingLimit: 10, // change to an appropriate limit number
-      excludeUnderUserPage: true,
-      excludeUnderTrashPage: true,
+      excludeUserPages: true,
+      excludeTrashPages: true,
     };
 
     this.changeURL = this.changeURL.bind(this);
@@ -44,8 +44,8 @@ class SearchPage extends React.Component {
     this.searchHandler = this.searchHandler.bind(this);
     this.selectPage = this.selectPage.bind(this);
     this.toggleCheckBox = this.toggleCheckBox.bind(this);
-    this.switchExcludingUnderUserPage = this.switchExcludingUnderUserPage.bind(this);
-    this.switchExcludingUnderTrashPage = this.switchExcludingUnderTrashPage.bind(this);
+    this.switchExcludingUserPagesHandler = this.switchExcludingUserPagesHandler.bind(this);
+    this.switchExcludingTrashPagesHandler = this.switchExcludingTrashPagesHandler.bind(this);
     this.onPagingNumberChanged = this.onPagingNumberChanged.bind(this);
   }
 
@@ -68,12 +68,12 @@ class SearchPage extends React.Component {
     return query;
   }
 
-  switchExcludingUnderUserPage() {
-    this.setState({ excludeUnderUserPage: !this.state.excludeUnderUserPage });
+  switchExcludingUserPagesHandler() {
+    this.setState({ excludeUserPages: !this.state.excludeUserPages });
   }
 
-  switchExcludingUnderTrashPage() {
-    this.setState({ excludeUnderTrashPage: !this.state.excludeUnderTrashPage });
+  switchExcludingTrashPagesHandler() {
+    this.setState({ excludeTrashPages: !this.state.excludeTrashPages });
   }
 
   changeURL(keyword, refreshHash) {
@@ -91,10 +91,10 @@ class SearchPage extends React.Component {
     let query = keyword;
 
     // pages included in specific path are not retrived when prefix is added
-    if (this.state.excludeUnderTrashPage) {
+    if (this.state.excludeTrashPages) {
       query = `${query} -prefix:${specificPathNames.trash}`;
     }
-    if (this.state.excludeUnderUserPage) {
+    if (this.state.excludeUserPages) {
       query = `${query} -prefix:${specificPathNames.user}`;
     }
 
@@ -226,8 +226,8 @@ class SearchPage extends React.Component {
         searchingKeyword={this.state.searchingKeyword}
         appContainer={this.props.appContainer}
         onSearchInvoked={this.searchHandler}
-        switchExcludingUnderUserPage={this.switchExcludingUnderUserPage}
-        switchExcludingUnderTrashPage={this.switchExcludingUnderTrashPage}
+        onSwitchExcludingUserPagesInvoked={this.switchExcludingUserPagesHandler}
+        onSwitchExcludingTrashPagesInvoked={this.switchExcludingTrashPagesHandler}
         excludeUnderUserPage={this.state.excludeUnderUserPage}
         excludeUnderTrashPage={this.state.excludeUnderTrashPage}
       >

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

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
 import SearchPageForm from './SearchPageForm';
 import AppContainer from '../../client/services/AppContainer';
 import DeleteSelectedPageGroup from './DeleteSelectedPageGroup';
-import FilterOptionModal from './FilterOptionModal';
+import SearchOptionModal from './SearchOptionModal';
 import { CheckboxType } from '../../interfaces/search';
 
 type Props = {
@@ -12,8 +12,8 @@ type Props = {
   excludeUnderUserPage: boolean,
   excludeUnderTrashPage: boolean,
   onSearchInvoked: (data: {keyword: string}) => boolean,
-  switchExcludingUnderUserPage?: () => void,
-  switchExcludingUnderTrashPage?: () => void,
+  onSwitchExcludingUserPagesInvoked?: () => void,
+  onSwitchExcludingTrashPagesInvoked?: () => void,
 }
 
 const SearchControl: FC <Props> = (props: Props) => {
@@ -24,15 +24,15 @@ const SearchControl: FC <Props> = (props: Props) => {
   const SearchPageFormTypeAny : any = SearchPageForm;
   const { t } = useTranslation('');
 
-  const switchExcludingUnderUserPage = () => {
-    if (props.switchExcludingUnderUserPage != null) {
-      props.switchExcludingUnderUserPage();
+  const switchExcludingUnderUserPageHandler = () => {
+    if (props.onSwitchExcludingUserPagesInvoked != null) {
+      props.onSwitchExcludingUserPagesInvoked();
     }
   };
 
-  const switchExcludingUnderTrashPage = () => {
-    if (props.switchExcludingUnderTrashPage != null) {
-      props.switchExcludingUnderTrashPage();
+  const switchExcludingUnderTrashPageHandler = () => {
+    if (props.onSwitchExcludingTrashPagesInvoked != null) {
+      props.onSwitchExcludingTrashPagesInvoked();
     }
   };
 
@@ -51,11 +51,11 @@ const SearchControl: FC <Props> = (props: Props) => {
     // ref: https://getbootstrap.com/docs/4.5/components/forms/#checkboxes
   };
 
-  const openFilterOptionModalHandler = () => {
+  const openSearchOptionModalHandler = () => {
     setIsFileterOptionModalShown(true);
   };
 
-  const closeFilterOptionModalHandler = () => {
+  const closeSearchOptionModalHandler = () => {
     setIsFileterOptionModalShown(false);
   };
 
@@ -65,14 +65,14 @@ const SearchControl: FC <Props> = (props: Props) => {
     }
   };
 
-  const rednerFilterOptionModal = () => {
+  const rednerSearchOptionModal = () => {
     return (
-      <FilterOptionModal
+      <SearchOptionModal
         isOpen={isFileterOptionModalShown || false}
         onClickFilteringSearchResult={onRetrySearchInvoked}
-        onClose={closeFilterOptionModalHandler}
-        switchExcludingUnderUserPage={switchExcludingUnderUserPage}
-        switchExcludingUnderTrashPage={switchExcludingUnderTrashPage}
+        onClose={closeSearchOptionModalHandler}
+        onSwitchExcludingUserPagesInvoked={switchExcludingUnderUserPageHandler}
+        onSwitchExcludingTrashPagesInvoked={switchExcludingUnderTrashPageHandler}
         excludeUnderUserPage={props.excludeUnderUserPage}
         excludeUnderTrashPage={props.excludeUnderTrashPage}
       />
@@ -109,7 +109,7 @@ const SearchControl: FC <Props> = (props: Props) => {
           <button
             type="button"
             className="btn"
-            onClick={openFilterOptionModalHandler}
+            onClick={openSearchOptionModalHandler}
           >
             <i className="icon-equalizer"></i>
           </button>
@@ -121,7 +121,7 @@ const SearchControl: FC <Props> = (props: Props) => {
                 className="mr-2"
                 type="checkbox"
                 id="flexCheckDefault"
-                onClick={() => switchExcludingUnderUserPage()}
+                onClick={switchExcludingUnderUserPageHandler}
               />
               {t('Include Subordinated Target Page', { target: '/user' })}
             </label>
@@ -132,14 +132,14 @@ const SearchControl: FC <Props> = (props: Props) => {
                 className="mr-2"
                 type="checkbox"
                 id="flexCheckChecked"
-                onClick={() => switchExcludingUnderTrashPage()}
+                onClick={switchExcludingUnderTrashPageHandler}
               />
               {t('Include Subordinated Target Page', { target: '/trash' })}
             </label>
           </div>
         </div>
       </div>
-      {rednerFilterOptionModal()}
+      {rednerSearchOptionModal()}
     </>
   );
 };

+ 8 - 8
packages/app/src/components/SearchPage/FilterOptionModal.tsx → packages/app/src/components/SearchPage/SearchOptionModal.tsx

@@ -11,12 +11,12 @@ type Props = {
   excludeUnderUserPage: boolean,
   excludeUnderTrashPage: boolean,
   onClose?: () => void,
-  switchExcludingUnderUserPage?: () => void,
-  switchExcludingUnderTrashPage?: () => void,
+  onSwitchExcludingUserPagesInvoked?: () => void,
+  onSwitchExcludingTrashPagesInvoked?: () => void,
   onClickFilteringSearchResult?: () => void,
 }
 
-const FilterOptionModal: FC<Props> = (props: Props) => {
+const SearchOptionModal: FC<Props> = (props: Props) => {
 
   const { t } = useTranslation('');
 
@@ -41,7 +41,7 @@ const FilterOptionModal: FC<Props> = (props: Props) => {
   return (
     <Modal size="lg" isOpen={isOpen} toggle={onCloseModal} autoFocus={false}>
       <ModalHeader tag="h4" toggle={onCloseModal} className="bg-primary text-light">
-        Filter Option
+        Search Option
       </ModalHeader>
       <ModalBody>
         <div className="d-flex justify-content-center mr-3">
@@ -50,7 +50,7 @@ const FilterOptionModal: FC<Props> = (props: Props) => {
               <input
                 className="mr-2"
                 type="checkbox"
-                onClick={switchExcludingUnderUserPage}
+                onClick={props.onSwitchExcludingUserPagesInvoked}
                 checked={!excludeUnderUserPage}
               />
               {t('Include Subordinated Target Page', { target: '/user' })}
@@ -61,7 +61,7 @@ const FilterOptionModal: FC<Props> = (props: Props) => {
               <input
                 className="mr-2"
                 type="checkbox"
-                onClick={switchExcludingUnderTrashPage}
+                onClick={props.onSwitchExcludingTrashPagesInvoked}
                 checked={!excludeUnderTrashPage}
               />
               {t('Include Subordinated Target Page', { target: '/trash' })}
@@ -74,11 +74,11 @@ const FilterOptionModal: FC<Props> = (props: Props) => {
           type="button"
           className="btn btn-secondary"
           onClick={onClickFilteringSearchResult}
-        >{t('search_result.narrow_donw')}
+        >{t('search_result.search_again')}
         </button>
       </ModalFooter>
     </Modal>
   );
 };
 
-export default FilterOptionModal;
+export default SearchOptionModal;

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

@@ -12,40 +12,34 @@ type Props ={
   focusedSearchResultData : IPageSearchResultData,
 }
 const SearchResultContent: FC<Props> = (props: Props) => {
+  const page = props.focusedSearchResultData?.pageData || {};
+  if (page == null) return null;
   // Temporaly workaround for lint error
   // 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 (
-
-    <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 - 5
packages/app/src/components/SearchPage/SearchResultListItem.tsx

@@ -7,12 +7,8 @@ import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
 import { IPageSearchResultData } from '../../interfaces/search';
 
-
-import loggerFactory from '~/utils/logger';
 import { IPageHasId } from '~/interfaces/page';
 
-const logger = loggerFactory('growi:searchResultList');
-
 type PageItemControlProps = {
   page: IPageHasId,
 }
@@ -80,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.
   const pageId = `#${pageData._id}`;
 
+  const isPathIncludedHtml = pageMeta.elasticSearchResult.highlightedPath != null;
   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) => {
     if (props.onClickInvoked != null) {

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

@@ -180,7 +180,7 @@ class SearchTypeahead extends React.Component {
     return (
       <span>
         <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} />
       </span>
     );

+ 1 - 3
packages/app/src/components/Sidebar/RecentChanges.jsx

@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
 
 import { useTranslation, withTranslation } from 'react-i18next';
 
-import { UserPicture } from '@growi/ui';
+import { UserPicture, FootstampIcon } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
 
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
@@ -16,8 +16,6 @@ import loggerFactory from '~/utils/logger';
 
 import LinkedPagePath from '~/models/linked-page-path';
 
-import FootstampIcon from '../FootstampIcon';
-
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 

+ 1 - 1
packages/app/src/components/User/SeenUserInfo.jsx

@@ -5,13 +5,13 @@ import React, { useState } from 'react';
 import {
   Button, Popover, PopoverBody,
 } from 'reactstrap';
+import { FootstampIcon } from '@growi/ui';
 import UserPictureList from './UserPictureList';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 import PageContainer from '~/client/services/PageContainer';
 
-import FootstampIcon from '../FootstampIcon';
 
 /* eslint react/no-multi-comp: 0, react/prop-types: 0 */
 

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

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

+ 2 - 0
packages/app/src/server/routes/search.js

@@ -179,6 +179,8 @@ module.exports = function(crowi, app) {
             elasticSearchResult: data.elasticSearchResult,
           };
 
+          pageData._doc.seenUserCount = (pageData.seenUsers && pageData.seenUsers.length) || 0;
+
           return { pageData, pageMeta };
         })
         .sort((page1, page2) => {

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

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

+ 8 - 0
packages/app/src/styles/_search.scss

@@ -195,6 +195,14 @@
             margin-right: 3px;
           }
         }
+        .page-list-meta {
+          > span {
+            margin-right: 12px;
+          }
+          .footstamp-icon {
+            margin-right: 2px;
+          }
+        }
       }
     }
 

+ 8 - 1
packages/app/src/styles/theme/_apply-colors.scss

@@ -18,7 +18,8 @@ $color-seen-user: #549c79 !default;
 $color-btn-reload-in-sidebar: $gray-500;
 $bgcolor-keyword-highlighted: $grw-marker-yellow !default;
 $bordercolor-search-item-left-active: $primary;
-$bgcolor-search-item-active: lighten($bordercolor-search-item-left-active, 76%);
+$bgcolor-search-item-active: lighten($bordercolor-search-item-left-active, 76%) !default;
+$color-search-item-pagelist-meta: $gray-500 !default;
 
 // override bootstrap variables
 $body-bg: $bgcolor-global;
@@ -600,6 +601,12 @@ body.pathname-sidebar {
           }
         }
       }
+      .page-list-meta {
+        color: $color-search-item-pagelist-meta;
+        svg {
+          fill: $color-search-item-pagelist-meta;
+        }
+      }
     }
   }
 }

+ 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
 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 {
 

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

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

+ 12 - 4
packages/ui/src/components/PagePath/PageListMeta.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { templateChecker, pagePathUtils } from '@growi/core';
+import { FootstampIcon } from '../SearchPage/FootstampIcon';
 
 const { isTopPage } = pagePathUtils;
 const { checkTemplatePath } = templateChecker;
@@ -37,16 +38,26 @@ export class PageListMeta extends React.Component {
       locked = <span><i className="icon-lock" /></span>;
     }
 
+    let seenUserCount;
+    if (page.seenUserCount > 0) {
+      seenUserCount = (
+        <span>
+          <i className="footstamp-icon"><FootstampIcon /></i>
+          {page.seenUsers.length}
+        </span>
+      );
+    }
+
     let bookmarkCount;
     if (this.props.bookmarkCount > 0) {
       bookmarkCount = <span><i className="icon-star" />{this.props.bookmarkCount}</span>;
     }
 
-
     return (
       <span className="page-list-meta">
         {topLabel}
         {templateLabel}
+        {seenUserCount}
         {commentCount}
         {likerCount}
         {locked}
@@ -61,6 +72,3 @@ PageListMeta.propTypes = {
   page: PropTypes.object.isRequired,
   bookmarkCount: PropTypes.number,
 };
-
-PageListMeta.defaultProps = {
-};

+ 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>;
+};

+ 1 - 3
packages/app/src/components/FootstampIcon.jsx → packages/ui/src/components/SearchPage/FootstampIcon.jsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-const FootstampIcon = () => (
+export const FootstampIcon = () => (
   <svg
     xmlns="http://www.w3.org/2000/svg"
     width="16"
@@ -27,5 +27,3 @@ const FootstampIcon = () => (
     <path d="M13.49,7.57a.81.81,0,0,0-.8.71l-.1.71a.82.82,0,0,0,.7.91h.11a.81.81,0,0,0,.8-.71l.1-.71a.81.81,0,0,0-.7-.91Z" />
   </svg>
 );
-
-export default FootstampIcon;

+ 1 - 0
packages/ui/src/index.ts

@@ -2,3 +2,4 @@ export * from './components/Attachment/Attachment';
 export * from './components/PagePath/PageListMeta';
 export * from './components/PagePath/PagePathLabel';
 export * from './components/User/UserPicture';
+export * from './components/SearchPage/FootstampIcon';