@@ -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>;
}
@@ -30,7 +30,7 @@ class SearchPage extends React.Component {
searchedKeyword: '',
searchResults: [],
searchResultMeta: {},
- focusedSearchResultData: {},
+ focusedSearchResultData: null,
selectedPages: new Set(),
searchResultCount: 0,
activePage: 1,
@@ -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}
- />
- );
- };
- 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}
+ />
);
};
@@ -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) {
@@ -180,7 +180,7 @@ class SearchTypeahead extends React.Component {
<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>
@@ -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 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';
@@ -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';
/* eslint react/no-multi-comp: 0, react/prop-types: 0 */
@@ -12,7 +12,7 @@ export type IPageSearchResultData = {
bookmarkCount: number,
elasticSearchResult: {
snippet: string,
- matchedPath: string,
+ highlightedPath: string,
},
@@ -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) => {
@@ -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;
@@ -195,6 +195,14 @@
margin-right: 3px;
+ .page-list-meta {
+ > span {
+ margin-right: 12px;
+ }
+ .footstamp-icon {
+ margin-right: 2px;
@@ -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 {
+ color: $color-search-item-pagelist-meta;
+ svg {
+ fill: $color-search-item-pagelist-meta;
@@ -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 {
@@ -13,7 +13,7 @@ export class PagePathWrapper extends React.Component {
- <PagePathLabel page={{ path: this.props.pagePath }} isLatterOnly additionalClassNames={classNames} />
+ <PagePathLabel path={this.props.pagePath} isLatterOnly additionalClassNames={classNames} />
@@ -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>;
<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 = {
-};
@@ -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></>;
-PagePathLabel.propTypes = {
- page: PropTypes.object.isRequired,
- isLatterOnly: PropTypes.bool,
- isFormerOnly: PropTypes.bool,
- additionalClassNames: PropTypes.arrayOf(PropTypes.string),
-PagePathLabel.defaultProps = {
- additionalClassNames: [],
@@ -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,6 +1,6 @@
-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;
@@ -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';