SearchResultListItem.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import React, { FC } from 'react';
  2. import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
  3. import { DevidedPagePath } from '@growi/core';
  4. import loggerFactory from '~/utils/logger';
  5. const logger = loggerFactory('growi:searchResultList');
  6. type Props ={
  7. page: {
  8. _id: string,
  9. path: string,
  10. noLink: boolean,
  11. lastUpdateUser: any
  12. elasticSearchResult: {
  13. snippet: string,
  14. }
  15. },
  16. onClickInvoked: (data: string) => void,
  17. }
  18. const MAX_SNIPPET_LENGTH = 180;
  19. const APPEND_SNIPPET_STRING = '...';
  20. const SearchResultListItem: FC<Props> = (props:Props) => {
  21. const { page, onClickInvoked } = props;
  22. // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
  23. const pageId = `#${page._id}`;
  24. const dPagePath = new DevidedPagePath(page.path, false, true);
  25. const pagePathElem = <PagePathLabel page={page} isFormerOnly />;
  26. const snippet = page.elasticSearchResult.snippet;
  27. const sliceSnippet = (snippet: string): string => {
  28. // when regex pattern is not matched with less than 300 characters slicedSnippet is null
  29. console.log('snippet', snippet);
  30. const slicedAndMatchedSnippet: string[] | null = snippet.slice(0, MAX_SNIPPET_LENGTH).match(/[\s\S]*<\/em>[^(<em)]*/g);
  31. return slicedAndMatchedSnippet == null
  32. ? `${snippet.slice(0, MAX_SNIPPET_LENGTH)}${APPEND_SNIPPET_STRING}` : `${slicedAndMatchedSnippet[0]}${APPEND_SNIPPET_STRING}`;
  33. };
  34. // TODO : send cetain length of body (revisionBody) from elastisearch by adding some settings to the query and
  35. // when keyword is not in page content, display revisionBody.
  36. // TASK : https://estoc.weseek.co.jp/redmine/issues/79606
  37. return (
  38. <li key={page._id} className="page-list-li search-page-item w-100 border-bottom pr-4">
  39. <a
  40. className="d-block pt-3"
  41. href={pageId}
  42. onClick={() => {
  43. try {
  44. if (onClickInvoked == null) { throw new Error('onClickInvoked is null') }
  45. onClickInvoked(page._id);
  46. }
  47. catch (error) {
  48. logger.error(error);
  49. }
  50. }}
  51. >
  52. <div className="d-flex">
  53. {/* checkbox */}
  54. <div className="form-check my-auto mx-2">
  55. <input className="form-check-input my-auto" type="checkbox" value="" id="flexCheckDefault" />
  56. </div>
  57. <div className="w-100">
  58. {/* page path */}
  59. <small className="mb-1">
  60. <i className="icon-fw icon-home"></i>
  61. {pagePathElem}
  62. </small>
  63. <div className="d-flex my-1 align-items-center">
  64. {/* page title */}
  65. <h3 className="mb-0">
  66. <UserPicture user={page.lastUpdateUser} />
  67. <span className="mx-2">{dPagePath.latter}</span>
  68. </h3>
  69. {/* page meta */}
  70. <div className="d-flex mx-2">
  71. <PageListMeta page={page} />
  72. </div>
  73. {/* doropdown icon */}
  74. <div className="ml-auto">
  75. <i className="fa fa-ellipsis-v text-muted"></i>
  76. </div>
  77. {/* Todo: add the following icon into dropdown menu */}
  78. {/* <button
  79. type="button"
  80. className="btn btn-link p-0"
  81. value={page.path}
  82. onClick={(e) => {
  83. window.location.href = e.currentTarget.value;
  84. }}
  85. >
  86. <i className="icon-login" />
  87. </button> */}
  88. </div>
  89. <div
  90. className="mt-1 mb-2"
  91. dangerouslySetInnerHTML={{ __html: snippet.length <= MAX_SNIPPET_LENGTH ? snippet : sliceSnippet(snippet) }}
  92. >
  93. </div>
  94. </div>
  95. </div>
  96. </a>
  97. </li>
  98. );
  99. };
  100. export default SearchResultListItem;