Yuki Takei 4 месяцев назад
Родитель
Сommit
a51a7a6579

+ 42 - 37
apps/app/src/client/components/PageSelectModal/PageSelectModal.tsx

@@ -3,30 +3,35 @@ import {
   Suspense, useState, useCallback, useMemo,
 } from 'react';
 
-import nodePath from 'path';
-
-import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
+import { dirname } from 'pathe';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter, Button,
 } from 'reactstrap';
-import SimpleBar from 'simplebar-react';
 
-import type { IPageForItem } from '~/interfaces/page';
+import { SimplifiedItemsTree } from '~/features/page-tree/components';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
 import { useCurrentPageData } from '~/states/page';
-import { usePageSelectModalStatus, usePageSelectModalActions } from '~/states/ui/modal/page-select';
+import {
+  usePageSelectModalStatus,
+  usePageSelectModalActions,
+  useSelectedPageInModal,
+} from '~/states/ui/modal/page-select';
 
-import { ItemsTree } from '../ItemsTree';
 import ItemsTreeContentSkeleton from '../ItemsTree/ItemsTreeContentSkeleton';
 
-import { TreeItemForModal } from './TreeItemForModal';
+import { SimplifiedTreeItemForModal, simplifiedTreeItemForModalSize } from './SimplifiedTreeItemForModal';
 
 const PageSelectModalSubstance: FC = () => {
   const { close: closeModal } = usePageSelectModalActions();
 
-  const [clickedParentPage, setClickedParentPage] = useState<IPageForItem | null>(null);
   const [isIncludeSubPage, setIsIncludeSubPage] = useState(true);
+  const [scrollerElem, setScrollerElem] = useState<HTMLDivElement | null>(null);
+
+  // Callback ref to capture the scroller element and trigger re-render
+  const scrollerRefCallback = useCallback((node: HTMLDivElement | null) => {
+    setScrollerElem(node);
+  }, []);
 
   const { t } = useTranslation();
 
@@ -35,41 +40,35 @@ const PageSelectModalSubstance: FC = () => {
   const currentPage = useCurrentPageData();
   const { opts } = usePageSelectModalStatus();
 
-  const isHierarchicalSelectionMode = opts?.isHierarchicalSelectionMode ?? false;
-
-  const onClickTreeItem = useCallback((page: IPageForItem) => {
-    const parentPagePath = page.path;
+  // Get selected page from atom
+  const selectedPage = useSelectedPageInModal();
 
-    if (parentPagePath == null) {
-      return;
-    }
-
-    setClickedParentPage(page);
-  }, []);
+  const isHierarchicalSelectionMode = opts?.isHierarchicalSelectionMode ?? false;
 
   const onClickCancel = useCallback(() => {
-    setClickedParentPage(null);
     closeModal();
   }, [closeModal]);
 
   const { onSelected } = opts ?? {};
   const onClickDone = useCallback(() => {
-    if (clickedParentPage != null) {
-      onSelected?.(clickedParentPage, isIncludeSubPage);
+    if (selectedPage != null) {
+      onSelected?.(selectedPage, isIncludeSubPage);
     }
 
     closeModal();
-  }, [clickedParentPage, closeModal, isIncludeSubPage, onSelected]);
+  }, [selectedPage, closeModal, isIncludeSubPage, onSelected]);
 
-  // Memoize heavy calculation
-  const parentPagePath = useMemo(() => (
-    pathUtils.addTrailingSlash(nodePath.dirname(currentPage?.path ?? ''))
-  ), [currentPage?.path]);
+  // Memoize heavy calculation - parent page path without trailing slash for matching
+  const parentPagePath = useMemo(() => {
+    const dn = dirname(currentPage?.path ?? '');
+    // Ensure root path is '/' not ''
+    return dn === '' ? '/' : dn;
+  }, [currentPage?.path]);
 
-  // Memoize target path calculation (avoid duplication)
+  // Memoize target path calculation
   const targetPath = useMemo(() => (
-    clickedParentPage?.path || parentPagePath
-  ), [clickedParentPage?.path, parentPagePath]);
+    selectedPage?.path || parentPagePath
+  ), [selectedPage?.path, parentPagePath]);
 
   // Memoize checkbox handler
   const handleIncludeSubPageChange = useCallback(() => {
@@ -85,18 +84,24 @@ const PageSelectModalSubstance: FC = () => {
       <ModalHeader toggle={closeModal}>{t('page_select_modal.select_page_location')}</ModalHeader>
       <ModalBody className="p-0">
         <Suspense fallback={<ItemsTreeContentSkeleton />}>
-          <SimpleBar style={{ maxHeight: 'calc(85vh - 133px)' }}> {/* 133px = 63px(ModalHeader) + 70px(ModalFooter) */}
-            <div className="p-3">
-              <ItemsTree
-                CustomTreeItem={TreeItemForModal}
+          {/* 133px = 63px(ModalHeader) + 70px(ModalFooter) */}
+          <div
+            ref={scrollerRefCallback}
+            className="p-3"
+            style={{ maxHeight: 'calc(85vh - 133px)', overflowY: 'auto' }}
+          >
+            {scrollerElem && (
+              <SimplifiedItemsTree
+                CustomTreeItem={SimplifiedTreeItemForModal}
                 isEnableActions={!isGuestUser}
                 isReadOnlyUser={!!isReadOnlyUser}
                 targetPath={targetPath}
                 targetPathOrId={targetPath}
-                onClickTreeItem={onClickTreeItem}
+                estimateTreeItemSize={() => simplifiedTreeItemForModalSize}
+                scrollerElem={scrollerElem}
               />
-            </div>
-          </SimpleBar>
+            )}
+          </div>
         </Suspense>
       </ModalBody>
       <ModalFooter className="border-top d-flex flex-column">

+ 52 - 0
apps/app/src/client/components/PageSelectModal/SimplifiedTreeItemForModal.tsx

@@ -0,0 +1,52 @@
+import type { FC } from 'react';
+import { useCallback, useMemo } from 'react';
+
+import type { TreeItemProps } from '~/features/page-tree';
+import { TreeItemLayout } from '~/features/page-tree/components';
+import type { IPageForItem } from '~/interfaces/page';
+import { useSelectPageInModal } from '~/states/ui/modal/page-select';
+
+import styles from './TreeItemForModal.module.scss';
+
+const moduleClass = styles['tree-item-for-modal'] ?? '';
+
+export const simplifiedTreeItemForModalSize = 36; // in px
+
+type SimplifiedTreeItemForModalProps = TreeItemProps & {
+  key?: React.Key | null;
+};
+
+export const SimplifiedTreeItemForModal: FC<SimplifiedTreeItemForModalProps> = (props) => {
+  const {
+    item,
+    targetPathOrId,
+    onToggle,
+  } = props;
+
+  const page = item.getItemData();
+  const selectPage = useSelectPageInModal();
+
+  // Determine if this item is selected
+  const isSelected = useMemo(() => {
+    return page._id === targetPathOrId || page.path === targetPathOrId;
+  }, [page._id, page.path, targetPathOrId]);
+
+  // Handle click to select this page
+  const handleClick = useCallback((selectedPage: IPageForItem) => {
+    selectPage(selectedPage);
+  }, [selectPage]);
+
+  const itemClassNames = [
+    isSelected ? 'active' : '',
+  ];
+
+  return (
+    <TreeItemLayout
+      {...props}
+      className={moduleClass}
+      itemClassName={itemClassNames.join(' ')}
+      onClick={handleClick}
+      onToggle={onToggle}
+    />
+  );
+};

+ 34 - 2
apps/app/src/states/ui/modal/page-select.ts

@@ -1,6 +1,8 @@
 import { useCallback } from 'react';
 import { atom, useAtomValue, useSetAtom } from 'jotai';
 
+import type { IPageForItem } from '~/interfaces/page';
+
 import type { OnSelectedFunction } from '../../../interfaces/ui';
 
 type IPageSelectModalOption = {
@@ -23,6 +25,9 @@ const pageSelectModalAtom = atom<PageSelectModalStatus>({
   isOpened: false,
 });
 
+// Atom for selected page in modal
+const selectedPageAtom = atom<IPageForItem | null>(null);
+
 /**
  * Hook for managing page select modal state
  * Returns read-only modal status for optimal performance
@@ -37,17 +42,44 @@ export const usePageSelectModalStatus = (): PageSelectModalStatus => {
  */
 export const usePageSelectModalActions = (): PageSelectModalActions => {
   const setStatus = useSetAtom(pageSelectModalAtom);
+  const setSelectedPage = useSetAtom(selectedPageAtom);
 
   const open = useCallback(
     (opts?: IPageSelectModalOption) => {
       setStatus({ isOpened: true, opts });
+      setSelectedPage(null); // Reset selected page when modal opens
     },
-    [setStatus],
+    [setStatus, setSelectedPage],
   );
 
   const close = useCallback(() => {
     setStatus({ isOpened: false, opts: undefined });
-  }, [setStatus]);
+    setSelectedPage(null); // Reset selected page when modal closes
+  }, [setStatus, setSelectedPage]);
 
   return { open, close };
 };
+
+/**
+ * Hook for getting selected page in modal
+ */
+export const useSelectedPageInModal = (): IPageForItem | null => {
+  return useAtomValue(selectedPageAtom);
+};
+
+/**
+ * Hook for selecting a page in modal
+ */
+export const useSelectPageInModal = (): ((page: IPageForItem) => void) => {
+  const setSelectedPage = useSetAtom(selectedPageAtom);
+
+  return useCallback(
+    (page: IPageForItem) => {
+      if (page.path == null) {
+        return;
+      }
+      setSelectedPage(page);
+    },
+    [setSelectedPage],
+  );
+};