PageSelectModal.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import type { FC, JSX } from 'react';
  2. import {
  3. Suspense, useState, useCallback, useMemo,
  4. } from 'react';
  5. import { useTranslation } from 'next-i18next';
  6. import { dirname } from 'pathe';
  7. import {
  8. Modal, ModalHeader, ModalBody, ModalFooter, Button,
  9. } from 'reactstrap';
  10. import { ItemsTree } from '~/features/page-tree/components';
  11. import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
  12. import { useCurrentPageData } from '~/states/page';
  13. import {
  14. usePageSelectModalStatus,
  15. usePageSelectModalActions,
  16. useSelectedPageInModal,
  17. } from '~/states/ui/modal/page-select';
  18. import ItemsTreeContentSkeleton from '../ItemsTree/ItemsTreeContentSkeleton';
  19. import { TreeItemForModal, treeItemForModalSize } from './TreeItemForModal';
  20. const PageSelectModalSubstance: FC = () => {
  21. const { close: closeModal } = usePageSelectModalActions();
  22. const [isIncludeSubPage, setIsIncludeSubPage] = useState(true);
  23. const [scrollerElem, setScrollerElem] = useState<HTMLDivElement | null>(null);
  24. // Callback ref to capture the scroller element and trigger re-render
  25. const scrollerRefCallback = useCallback((node: HTMLDivElement | null) => {
  26. setScrollerElem(node);
  27. }, []);
  28. const { t } = useTranslation();
  29. const isGuestUser = useIsGuestUser();
  30. const isReadOnlyUser = useIsReadOnlyUser();
  31. const currentPage = useCurrentPageData();
  32. const { opts } = usePageSelectModalStatus();
  33. // Get selected page from atom
  34. const selectedPage = useSelectedPageInModal();
  35. const isHierarchicalSelectionMode = opts?.isHierarchicalSelectionMode ?? false;
  36. const onClickCancel = useCallback(() => {
  37. closeModal();
  38. }, [closeModal]);
  39. const { onSelected } = opts ?? {};
  40. const onClickDone = useCallback(() => {
  41. if (selectedPage != null) {
  42. onSelected?.(selectedPage, isIncludeSubPage);
  43. }
  44. closeModal();
  45. }, [selectedPage, closeModal, isIncludeSubPage, onSelected]);
  46. // Memoize heavy calculation - parent page path without trailing slash for matching
  47. const parentPagePath = useMemo(() => {
  48. const dn = dirname(currentPage?.path ?? '');
  49. // Ensure root path is '/' not ''
  50. return dn === '' ? '/' : dn;
  51. }, [currentPage?.path]);
  52. // Memoize target path calculation
  53. const targetPath = useMemo(() => (
  54. selectedPage?.path || parentPagePath
  55. ), [selectedPage?.path, parentPagePath]);
  56. // Memoize checkbox handler
  57. const handleIncludeSubPageChange = useCallback(() => {
  58. setIsIncludeSubPage(!isIncludeSubPage);
  59. }, [isIncludeSubPage]);
  60. if (isGuestUser == null) {
  61. return <></>;
  62. }
  63. return (
  64. <>
  65. <ModalHeader toggle={closeModal}>{t('page_select_modal.select_page_location')}</ModalHeader>
  66. <ModalBody className="p-0">
  67. <Suspense fallback={<ItemsTreeContentSkeleton />}>
  68. {/* 133px = 63px(ModalHeader) + 70px(ModalFooter) */}
  69. <div
  70. ref={scrollerRefCallback}
  71. className="p-3"
  72. style={{ maxHeight: 'calc(85vh - 133px)', overflowY: 'auto' }}
  73. >
  74. {scrollerElem && (
  75. <ItemsTree
  76. CustomTreeItem={TreeItemForModal}
  77. isEnableActions={!isGuestUser}
  78. isReadOnlyUser={!!isReadOnlyUser}
  79. targetPath={targetPath}
  80. targetPathOrId={targetPath}
  81. estimateTreeItemSize={() => treeItemForModalSize}
  82. scrollerElem={scrollerElem}
  83. />
  84. )}
  85. </div>
  86. </Suspense>
  87. </ModalBody>
  88. <ModalFooter className="border-top d-flex flex-column">
  89. { isHierarchicalSelectionMode && (
  90. <div className="form-check form-check-info align-self-start ms-4">
  91. <input
  92. type="checkbox"
  93. id="includeSubPages"
  94. className="form-check-input"
  95. name="fileUpload"
  96. checked={isIncludeSubPage}
  97. onChange={handleIncludeSubPageChange}
  98. />
  99. <label
  100. className="form-label form-check-label"
  101. htmlFor="includeSubPages"
  102. >
  103. {t('Include Subordinated Page')}
  104. </label>
  105. </div>
  106. )}
  107. <div className="d-flex gap-2 align-self-end">
  108. <Button color="secondary" onClick={onClickCancel}>{t('Cancel')}</Button>
  109. <Button color="primary" onClick={onClickDone}>{t('Done')}</Button>
  110. </div>
  111. </ModalFooter>
  112. </>
  113. );
  114. };
  115. export const PageSelectModal = (): JSX.Element => {
  116. const pageSelectModalData = usePageSelectModalStatus();
  117. const { close: closePageSelectModal } = usePageSelectModalActions();
  118. const isOpen = pageSelectModalData?.isOpened ?? false;
  119. if (!isOpen) {
  120. return <></>;
  121. }
  122. return (
  123. <Modal isOpen={isOpen} toggle={closePageSelectModal} centered>
  124. <PageSelectModalSubstance />
  125. </Modal>
  126. );
  127. };