SearchResultList.tsx 4.6 KB

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