SearchResultList.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import React, {
  2. forwardRef,
  3. ForwardRefRenderFunction, useCallback, useImperativeHandle, useRef,
  4. } from 'react';
  5. import { useTranslation } from 'react-i18next';
  6. import { ISelectable, ISelectableAll } from '~/client/interfaces/selectable-all';
  7. import { toastSuccess } from '~/client/util/apiNotification';
  8. import {
  9. IPageInfoForListing, IPageWithMeta, isIPageInfoForListing,
  10. } from '~/interfaces/page';
  11. import { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
  12. import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
  13. import { useIsGuestUser } from '~/stores/context';
  14. import { useSWRxPageInfoForList } from '~/stores/page';
  15. import { usePageTreeTermManager } from '~/stores/page-listing';
  16. import { useFullTextSearchTermManager } from '~/stores/search';
  17. import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
  18. import { PageListItemL } from '../PageList/PageListItemL';
  19. type Props = {
  20. pages: IPageWithSearchMeta[],
  21. selectedPageId?: string,
  22. forceHideMenuItems?: ForceHideMenuItems,
  23. onPageSelected?: (page?: IPageWithSearchMeta) => void,
  24. onCheckboxChanged?: (isChecked: boolean, pageId: string) => void,
  25. }
  26. const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props> = (props:Props, ref) => {
  27. const {
  28. pages, selectedPageId,
  29. forceHideMenuItems,
  30. onPageSelected,
  31. } = props;
  32. const { t } = useTranslation();
  33. const pageIdsWithNoSnippet = pages
  34. .filter(page => (page.meta?.elasticSearchResult?.snippet?.length ?? 0) === 0)
  35. .map(page => page.data._id);
  36. const { data: isGuestUser } = useIsGuestUser();
  37. const { data: idToPageInfo } = useSWRxPageInfoForList(pageIdsWithNoSnippet, true, true);
  38. // for mutation
  39. const { advance: advancePt } = usePageTreeTermManager();
  40. const { advance: advanceFts } = useFullTextSearchTermManager();
  41. const itemsRef = useRef<(ISelectable|null)[]>([]);
  42. // publish selectAll()
  43. useImperativeHandle(ref, () => ({
  44. selectAll: () => {
  45. const items = itemsRef.current;
  46. if (items != null) {
  47. items.forEach(item => item != null && item.select());
  48. }
  49. },
  50. deselectAll: () => {
  51. const items = itemsRef.current;
  52. if (items != null) {
  53. items.forEach(item => item != null && item.deselect());
  54. }
  55. },
  56. }));
  57. const clickItemHandler = useCallback((pageId: string) => {
  58. if (onPageSelected != null) {
  59. const selectedPage = pages.find(page => page.data._id === pageId);
  60. onPageSelected(selectedPage);
  61. }
  62. }, [onPageSelected, pages]);
  63. let injectedPages: (IPageWithSearchMeta | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>)[] | undefined;
  64. // inject data to list
  65. if (idToPageInfo != null) {
  66. injectedPages = pages.map((page) => {
  67. const pageInfo = idToPageInfo[page.data._id];
  68. if (!isIPageInfoForListing(pageInfo)) {
  69. // return as is
  70. return page;
  71. }
  72. return {
  73. data: page.data,
  74. meta: {
  75. ...page.meta,
  76. ...pageInfo,
  77. },
  78. } as IPageWithMeta<IPageInfoForListing & IPageSearchMeta>;
  79. });
  80. }
  81. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  82. const duplicatedHandler : OnDuplicatedFunction = (fromPath, toPath) => {
  83. toastSuccess(t('duplicated_pages', { fromPath }));
  84. advancePt();
  85. advanceFts();
  86. };
  87. const renamedHandler: OnRenamedFunction = (path) => {
  88. toastSuccess(t('renamed_pages', { path }));
  89. advancePt();
  90. advanceFts();
  91. };
  92. const deletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
  93. if (typeof pathOrPathsToDelete !== 'string') {
  94. return;
  95. }
  96. const path = pathOrPathsToDelete;
  97. if (isCompletely) {
  98. toastSuccess(t('deleted_pages_completely', { path }));
  99. }
  100. else {
  101. toastSuccess(t('deleted_pages', { path }));
  102. }
  103. advancePt();
  104. advanceFts();
  105. };
  106. return (
  107. <ul data-testid="search-result-list" className="page-list-ul list-group list-group-flush">
  108. { (injectedPages ?? pages).map((page, i) => {
  109. return (
  110. <PageListItemL
  111. key={page.data._id}
  112. // eslint-disable-next-line no-return-assign
  113. ref={c => itemsRef.current[i] = c}
  114. page={page}
  115. isEnableActions={!isGuestUser}
  116. isSelected={page.data._id === selectedPageId}
  117. forceHideMenuItems={forceHideMenuItems}
  118. onClickItem={clickItemHandler}
  119. onCheckboxChanged={props.onCheckboxChanged}
  120. onPageDuplicated={duplicatedHandler}
  121. onPageRenamed={renamedHandler}
  122. onPageDeleted={deletedHandler}
  123. />
  124. );
  125. })}
  126. </ul>
  127. );
  128. };
  129. export const SearchResultList = forwardRef(SearchResultListSubstance);