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

Merge branch 'feat/search-implement' into feat/77524-search-result-conent-page

* feat/search-implement: (32 commits)
  81922 add default value
  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
  77804 change var name
  77804 add default
  77804 import footstamp from packages/ui
  77804 change code position to prioritise styles of  _search.scss over _page_list.scss
  77804 change icon color
  77804 add @growi/app to import footstamp component
  77804 add null check
  77804 add condition to show footprint
  77804 change footstamp's color and margin
  77804 add tentative foot print
  refactor as ts
  typescriptize
  ...
Mao 4 лет назад
Родитель
Сommit
ef93dc2f59

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

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

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

+ 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="col-lg-6  page-list border boder-gray search-result-list px-0" id="search-result-list">
         <div className="col-lg-6  page-list border boder-gray search-result-list px-0" id="search-result-list">
 
 
           <nav><SearchControl></SearchControl></nav>
           <nav><SearchControl></SearchControl></nav>
-          <div className="d-flex align-items-start justify-content-between mt-1">
-            <div className="search-result-meta">
+          <div className="d-flex align-items-center justify-content-between mt-1 mb-3">
+            <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">

+ 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 { DevidedPagePath } from '@growi/core';
 import { IPageSearchResultData } from '../../interfaces/search';
 import { IPageSearchResultData } from '../../interfaces/search';
 
 
-
-import loggerFactory from '~/utils/logger';
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
 
 
-const logger = loggerFactory('growi:searchResultList');
-
 type PageItemControlProps = {
 type PageItemControlProps = {
   page: IPageHasId,
   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.
   // 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 - 3
packages/app/src/components/Sidebar/RecentChanges.jsx

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

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

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

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

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

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

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

@@ -195,14 +195,25 @@
             margin-right: 3px;
             margin-right: 3px;
           }
           }
         }
         }
+        .page-list-meta {
+          > span {
+            margin-right: 12px;
+          }
+          .footstamp-icon {
+            margin-right: 2px;
+          }
+        }
       }
       }
     }
     }
 
 
     .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;

+ 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;
 $color-btn-reload-in-sidebar: $gray-500;
 $bgcolor-keyword-highlighted: $grw-marker-yellow !default;
 $bgcolor-keyword-highlighted: $grw-marker-yellow !default;
 $bordercolor-search-item-left-active: $primary;
 $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
 // override bootstrap variables
 $body-bg: $bgcolor-global;
 $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
 // 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} />
     );
     );
   }
   }
 
 

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

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { templateChecker, pagePathUtils } from '@growi/core';
 import { templateChecker, pagePathUtils } from '@growi/core';
+import { FootstampIcon } from '../SearchPage/FootstampIcon';
 
 
 const { isTopPage } = pagePathUtils;
 const { isTopPage } = pagePathUtils;
 const { checkTemplatePath } = templateChecker;
 const { checkTemplatePath } = templateChecker;
@@ -37,16 +38,26 @@ export class PageListMeta extends React.Component {
       locked = <span><i className="icon-lock" /></span>;
       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;
     let bookmarkCount;
     if (this.props.bookmarkCount > 0) {
     if (this.props.bookmarkCount > 0) {
       bookmarkCount = <span><i className="icon-star" />{this.props.bookmarkCount}</span>;
       bookmarkCount = <span><i className="icon-star" />{this.props.bookmarkCount}</span>;
     }
     }
 
 
-
     return (
     return (
       <span className="page-list-meta">
       <span className="page-list-meta">
         {topLabel}
         {topLabel}
         {templateLabel}
         {templateLabel}
+        {seenUserCount}
         {commentCount}
         {commentCount}
         {likerCount}
         {likerCount}
         {locked}
         {locked}
@@ -61,6 +72,3 @@ PageListMeta.propTypes = {
   page: PropTypes.object.isRequired,
   page: PropTypes.object.isRequired,
   bookmarkCount: PropTypes.number,
   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';
 import React from 'react';
 
 
-const FootstampIcon = () => (
+export const FootstampIcon = () => (
   <svg
   <svg
     xmlns="http://www.w3.org/2000/svg"
     xmlns="http://www.w3.org/2000/svg"
     width="16"
     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" />
     <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>
   </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/PageListMeta';
 export * from './components/PagePath/PagePathLabel';
 export * from './components/PagePath/PagePathLabel';
 export * from './components/User/UserPicture';
 export * from './components/User/UserPicture';
+export * from './components/SearchPage/FootstampIcon';