TrashPageAlert.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { type JSX, useCallback } from 'react';
  2. import { useRouter } from 'next/router';
  3. import { UserPicture } from '@growi/ui/dist/components';
  4. import { format } from 'date-fns/format';
  5. import { useTranslation } from 'react-i18next';
  6. import {
  7. useCurrentPageData,
  8. useCurrentPagePath,
  9. useFetchCurrentPage,
  10. useIsTrashPage,
  11. } from '~/states/page';
  12. import { usePageDeleteModalActions } from '~/states/ui/modal/page-delete';
  13. import { usePutBackPageModalActions } from '~/states/ui/modal/put-back-page';
  14. import { useIsAbleToShowTrashPageManagementButtons } from '~/states/ui/page-abilities';
  15. import { useSWRxPageInfo } from '~/stores/page';
  16. import { mutateRecentlyUpdated } from '~/stores/page-listing';
  17. const onDeletedHandler = (pathOrPathsToDelete) => {
  18. if (typeof pathOrPathsToDelete !== 'string') {
  19. return;
  20. }
  21. window.location.href = '/';
  22. };
  23. type SubstanceProps = {
  24. pageId: string;
  25. pagePath: string;
  26. revisionId: string;
  27. };
  28. const TrashPageAlertSubstance = (props: SubstanceProps): JSX.Element => {
  29. const { t } = useTranslation();
  30. const router = useRouter();
  31. const { pageId, pagePath, revisionId } = props;
  32. const pageData = useCurrentPageData();
  33. const isAbleToShowTrashPageManagementButtons =
  34. useIsAbleToShowTrashPageManagementButtons();
  35. // useSWRxPageInfo is executed only when Substance is rendered
  36. const { data: pageInfo } = useSWRxPageInfo(pageId);
  37. const { open: openDeleteModal } = usePageDeleteModalActions();
  38. const { open: openPutBackPageModal } = usePutBackPageModalActions();
  39. const currentPagePath = useCurrentPagePath();
  40. const { fetchCurrentPage } = useFetchCurrentPage();
  41. const deleteUser = pageData?.deleteUser;
  42. const deletedAt = pageData?.deletedAt
  43. ? format(new Date(pageData.deletedAt), 'yyyy/MM/dd HH:mm')
  44. : '';
  45. const openPutbackPageModalHandler = useCallback(() => {
  46. const putBackedHandler = async () => {
  47. if (currentPagePath == null) {
  48. return;
  49. }
  50. try {
  51. // biome-ignore lint/style/noRestrictedImports: no-problem dynamic import
  52. const unlink = (await import('~/client/services/page-operation'))
  53. .unlink;
  54. unlink(currentPagePath);
  55. router.push(`/${pageId}`);
  56. fetchCurrentPage();
  57. mutateRecentlyUpdated();
  58. } catch (err) {
  59. // biome-ignore lint/style/noRestrictedImports: no-problem dynamic import
  60. const toastError = (await import('~/client/util/toastr')).toastError;
  61. toastError(err);
  62. }
  63. };
  64. openPutBackPageModal(
  65. { pageId, path: pagePath },
  66. { onPutBacked: putBackedHandler },
  67. );
  68. }, [
  69. openPutBackPageModal,
  70. pageId,
  71. pagePath,
  72. currentPagePath,
  73. router,
  74. fetchCurrentPage,
  75. ]);
  76. const openPageDeleteModalHandler = useCallback(() => {
  77. const pageToDelete = {
  78. data: {
  79. _id: pageId,
  80. revision: revisionId,
  81. path: pagePath,
  82. },
  83. meta: pageInfo,
  84. };
  85. openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
  86. }, [openDeleteModal, pageId, pageInfo, pagePath, revisionId]);
  87. const renderTrashPageManagementButtons = useCallback(() => {
  88. return (
  89. <>
  90. <button
  91. type="button"
  92. className="btn btn-info rounded-pill btn-sm ms-auto me-2"
  93. onClick={openPutbackPageModalHandler}
  94. data-testid="put-back-button"
  95. >
  96. <span className="material-symbols-outlined" aria-hidden="true">
  97. undo
  98. </span>{' '}
  99. {t('Put Back')}
  100. </button>
  101. <button
  102. type="button"
  103. className="btn btn-danger rounded-pill btn-sm"
  104. disabled={!(pageInfo?.isAbleToDeleteCompletely ?? false)}
  105. onClick={openPageDeleteModalHandler}
  106. >
  107. <span className="material-symbols-outlined" aria-hidden="true">
  108. delete_forever
  109. </span>{' '}
  110. {t('Delete Completely')}
  111. </button>
  112. </>
  113. );
  114. }, [
  115. openPageDeleteModalHandler,
  116. openPutbackPageModalHandler,
  117. pageInfo?.isAbleToDeleteCompletely,
  118. t,
  119. ]);
  120. return (
  121. <div
  122. className="alert alert-warning py-3 ps-4 d-flex flex-column flex-lg-row"
  123. data-testid="trash-page-alert"
  124. >
  125. <div className="flex-grow-1">
  126. This page is in the trash{' '}
  127. <span className="material-symbols-outlined" aria-hidden="true">
  128. delete
  129. </span>
  130. .
  131. <br />
  132. <UserPicture user={deleteUser} />
  133. <span className="ms-2">
  134. Deleted by {deleteUser?.name} at{' '}
  135. <span data-vrt-blackout-datetime>
  136. {deletedAt ?? pageData?.updatedAt}
  137. </span>
  138. </span>
  139. </div>
  140. <div className="pt-1 d-flex align-items-end align-items-lg-center">
  141. {isAbleToShowTrashPageManagementButtons &&
  142. renderTrashPageManagementButtons()}
  143. </div>
  144. </div>
  145. );
  146. };
  147. export const TrashPageAlert = (): JSX.Element => {
  148. const pageData = useCurrentPageData();
  149. const isTrashPage = useIsTrashPage();
  150. const pageId = pageData?._id;
  151. const pagePath = pageData?.path;
  152. const revisionId = pageData?.revision?._id;
  153. // Lightweight condition checks in Container
  154. const isEmptyPage = pageId == null || revisionId == null || pagePath == null;
  155. // Show this alert only for non-empty pages in trash.
  156. if (!isTrashPage || isEmptyPage) {
  157. // biome-ignore lint/complexity/noUselessFragments: ignore
  158. return <></>;
  159. }
  160. // Render Substance only when conditions are met
  161. // useSWRxPageInfo will be executed only here
  162. return (
  163. <TrashPageAlertSubstance
  164. pageId={pageId}
  165. pagePath={pagePath}
  166. revisionId={revisionId}
  167. />
  168. );
  169. };