SearchResultList.tsx 4.5 KB

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