Просмотр исходного кода

refactor: implement page tree selection hooks and components for better modularity

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

+ 22 - 116
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageTreeSelection.tsx

@@ -1,46 +1,25 @@
-import { Suspense, useCallback, useMemo, useState } from 'react';
+import { useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 import { ModalBody } from 'reactstrap';
-import SimpleBar from 'simplebar-react';
 
-import ItemsTreeContentSkeleton from '~/client/components/ItemsTree/ItemsTreeContentSkeleton';
-import { SimplifiedItemsTree } from '~/features/page-tree/components';
-import type { IPageForTreeItem } from '~/interfaces/page';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
 
-import {
-  isSelectablePage,
-  type SelectablePage,
-} from '../../../../interfaces/selectable-page';
-import { useSelectedPages } from '../../../services/use-selected-pages';
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
 import {
   AiAssistantManagementModalPageMode,
   useAiAssistantManagementModalActions,
   useAiAssistantManagementModalStatus,
 } from '../../../states/modal/ai-assistant-management';
 import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
-import { SelectablePageList } from './SelectablePageList';
-import {
-  SimplifiedTreeItemWithCheckbox,
-  simplifiedTreeItemWithCheckboxSize,
-} from './SimplifiedTreeItemWithCheckbox';
+import { usePageTreeSelection } from './hooks/use-page-tree-selection';
+import { PageTreeSelectionTree } from './PageTreeSelectionTree';
+import { SelectedPagesPanel } from './SelectedPagesPanel';
 
 import styles from './AiAssistantManagementPageTreeSelection.module.scss';
 
 const moduleClass =
   styles['grw-ai-assistant-management-page-tree-selection'] ?? '';
 
-/**
- * Convert a page path to a glob pattern for selecting descendants.
- * Handles the root page case where '//*' should become '/*'.
- */
-const toPagePathGlob = (path: string): string => {
-  if (path === '/') {
-    return '/*';
-  }
-  return `${path}/*`;
-};
-
 type Props = {
   baseSelectedPages: SelectablePage[];
   updateBaseSelectedPages: (pages: SelectablePage[]) => void;
@@ -59,54 +38,13 @@ export const AiAssistantManagementPageTreeSelection = (
   const isNewAiAssistant =
     aiAssistantManagementModalData?.aiAssistantData == null;
 
-  // Scroll container for virtualization
-  const [scrollerElem, setScrollerElem] = useState<HTMLElement | null>(null);
-
-  const { selectedPages, selectedPagesArray, addPage, removePage } =
-    useSelectedPages(baseSelectedPages);
-
-  // Calculate initial checked items from baseSelectedPages
-  // Remove the /* suffix to match with page IDs
-  const initialCheckedItems = useMemo(() => {
-    return baseSelectedPages
-      .filter((page) => page._id != null)
-      .map((page) => page._id as string);
-  }, [baseSelectedPages]);
-
-  // Handle checked items change from tree
-  const handleCheckedItemsChange = useCallback(
-    (checkedPages: IPageForTreeItem[]) => {
-      // Get current checked page IDs (with /* suffix paths)
-      const currentCheckedPaths = new Set(
-        checkedPages
-          .filter((page) => isSelectablePage(page) && page.path != null)
-          .map((page) => toPagePathGlob(page.path as string)),
-      );
-
-      // Get currently selected page paths
-      const currentSelectedPaths = new Set(selectedPages.keys());
-
-      // Add newly checked pages
-      checkedPages.forEach((page) => {
-        if (!isSelectablePage(page) || page.path == null) {
-          return;
-        }
-        const pagePathWithGlob = toPagePathGlob(page.path);
-        if (!currentSelectedPaths.has(pagePathWithGlob)) {
-          const clonedPage = { ...page, path: pagePathWithGlob };
-          addPage(clonedPage as SelectablePage);
-        }
-      });
-
-      // Remove unchecked pages
-      selectedPagesArray.forEach((page) => {
-        if (page.path != null && !currentCheckedPaths.has(page.path)) {
-          removePage(page);
-        }
-      });
-    },
-    [selectedPages, selectedPagesArray, addPage, removePage],
-  );
+  const {
+    selectedPages,
+    selectedPagesArray,
+    initialCheckedItems,
+    handleCheckedItemsChange,
+    removePage,
+  } = usePageTreeSelection(baseSelectedPages);
 
   const nextButtonClickHandler = useCallback(() => {
     updateBaseSelectedPages(Array.from(selectedPages.values()));
@@ -122,11 +60,6 @@ export const AiAssistantManagementPageTreeSelection = (
     updateBaseSelectedPages,
   ]);
 
-  const estimateTreeItemSize = useCallback(
-    () => simplifiedTreeItemWithCheckboxSize,
-    [],
-  );
-
   return (
     <div className={moduleClass}>
       <AiAssistantManagementHeader
@@ -149,45 +82,18 @@ export const AiAssistantManagementPageTreeSelection = (
         </h4>
 
         <div className="px-4">
-          <div className="page-tree-container" ref={setScrollerElem}>
-            {scrollerElem != null && (
-              <Suspense fallback={<ItemsTreeContentSkeleton />}>
-                <SimplifiedItemsTree
-                  targetPath="/"
-                  isEnableActions={!isGuestUser}
-                  isReadOnlyUser={!!isReadOnlyUser}
-                  CustomTreeItem={SimplifiedTreeItemWithCheckbox}
-                  estimateTreeItemSize={estimateTreeItemSize}
-                  scrollerElem={scrollerElem}
-                  enableCheckboxes
-                  initialCheckedItems={initialCheckedItems}
-                  onCheckedItemsChange={handleCheckedItemsChange}
-                />
-              </Suspense>
-            )}
-          </div>
+          <PageTreeSelectionTree
+            isEnableActions={!isGuestUser}
+            isReadOnlyUser={!!isReadOnlyUser}
+            initialCheckedItems={initialCheckedItems}
+            onCheckedItemsChange={handleCheckedItemsChange}
+          />
         </div>
 
-        <h4 className="text-center fw-bold mb-3 mt-4">
-          {t('modal_ai_assistant.reference_pages')}
-        </h4>
-
-        <div className="px-4">
-          <SimpleBar
-            className="page-list-container"
-            style={{ maxHeight: '300px' }}
-          >
-            <SelectablePageList
-              method="remove"
-              methodButtonPosition="right"
-              pages={selectedPagesArray}
-              onClickMethodButton={removePage}
-            />
-          </SimpleBar>
-          <span className="form-text text-muted mt-2">
-            {t('modal_ai_assistant.can_add_later')}
-          </span>
-        </div>
+        <SelectedPagesPanel
+          pages={selectedPagesArray}
+          onRemovePage={removePage}
+        />
 
         <div className="d-flex justify-content-center mt-4">
           <button

+ 54 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/PageTreeSelectionTree.tsx

@@ -0,0 +1,54 @@
+import { Suspense, useCallback, useState } from 'react';
+
+import ItemsTreeContentSkeleton from '~/client/components/ItemsTree/ItemsTreeContentSkeleton';
+import { SimplifiedItemsTree } from '~/features/page-tree/components';
+import type { IPageForTreeItem } from '~/interfaces/page';
+
+import {
+  SimplifiedTreeItemWithCheckbox,
+  simplifiedTreeItemWithCheckboxSize,
+} from './SimplifiedTreeItemWithCheckbox';
+
+type Props = {
+  isEnableActions: boolean;
+  isReadOnlyUser: boolean;
+  initialCheckedItems: string[];
+  onCheckedItemsChange: (checkedPages: IPageForTreeItem[]) => void;
+};
+
+export const PageTreeSelectionTree = (props: Props): JSX.Element => {
+  const {
+    isEnableActions,
+    isReadOnlyUser,
+    initialCheckedItems,
+    onCheckedItemsChange,
+  } = props;
+
+  // Scroll container for virtualization
+  const [scrollerElem, setScrollerElem] = useState<HTMLElement | null>(null);
+
+  const estimateTreeItemSize = useCallback(
+    () => simplifiedTreeItemWithCheckboxSize,
+    [],
+  );
+
+  return (
+    <div className="page-tree-container" ref={setScrollerElem}>
+      {scrollerElem != null && (
+        <Suspense fallback={<ItemsTreeContentSkeleton />}>
+          <SimplifiedItemsTree
+            targetPath="/"
+            isEnableActions={isEnableActions}
+            isReadOnlyUser={isReadOnlyUser}
+            CustomTreeItem={SimplifiedTreeItemWithCheckbox}
+            estimateTreeItemSize={estimateTreeItemSize}
+            scrollerElem={scrollerElem}
+            enableCheckboxes
+            initialCheckedItems={initialCheckedItems}
+            onCheckedItemsChange={onCheckedItemsChange}
+          />
+        </Suspense>
+      )}
+    </div>
+  );
+};

+ 40 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectedPagesPanel.tsx

@@ -0,0 +1,40 @@
+import { useTranslation } from 'react-i18next';
+import SimpleBar from 'simplebar-react';
+
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
+import { SelectablePageList } from './SelectablePageList';
+
+type Props = {
+  pages: SelectablePage[];
+  onRemovePage: (page: SelectablePage) => void;
+};
+
+export const SelectedPagesPanel = (props: Props): JSX.Element => {
+  const { pages, onRemovePage } = props;
+  const { t } = useTranslation();
+
+  return (
+    <>
+      <h4 className="text-center fw-bold mb-3 mt-4">
+        {t('modal_ai_assistant.reference_pages')}
+      </h4>
+
+      <div className="px-4">
+        <SimpleBar
+          className="page-list-container"
+          style={{ maxHeight: '300px' }}
+        >
+          <SelectablePageList
+            method="remove"
+            methodButtonPosition="right"
+            pages={pages}
+            onClickMethodButton={onRemovePage}
+          />
+        </SimpleBar>
+        <span className="form-text text-muted mt-2">
+          {t('modal_ai_assistant.can_add_later')}
+        </span>
+      </div>
+    </>
+  );
+};

+ 88 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/hooks/use-page-tree-selection.ts

@@ -0,0 +1,88 @@
+import { useCallback, useMemo } from 'react';
+
+import type { IPageForTreeItem } from '~/interfaces/page';
+
+import {
+  isSelectablePage,
+  type SelectablePage,
+} from '../../../../../interfaces/selectable-page';
+import { useSelectedPages } from '../../../../services/use-selected-pages';
+
+/**
+ * Convert a page path to a glob pattern for selecting descendants.
+ * Handles the root page case where '//*' should become '/*'.
+ */
+export const toPagePathGlob = (path: string): string => {
+  if (path === '/') {
+    return '/*';
+  }
+  return `${path}/*`;
+};
+
+type UsePageTreeSelectionReturn = {
+  selectedPages: Map<string, SelectablePage>;
+  selectedPagesArray: SelectablePage[];
+  initialCheckedItems: string[];
+  handleCheckedItemsChange: (checkedPages: IPageForTreeItem[]) => void;
+  addPage: (page: SelectablePage) => void;
+  removePage: (page: SelectablePage) => void;
+};
+
+export const usePageTreeSelection = (
+  baseSelectedPages: SelectablePage[],
+): UsePageTreeSelectionReturn => {
+  const { selectedPages, selectedPagesArray, addPage, removePage } =
+    useSelectedPages(baseSelectedPages);
+
+  // Calculate initial checked items from baseSelectedPages
+  // Remove the /* suffix to match with page IDs
+  const initialCheckedItems = useMemo(() => {
+    return baseSelectedPages
+      .filter((page) => page._id != null)
+      .map((page) => page._id as string);
+  }, [baseSelectedPages]);
+
+  // Handle checked items change from tree
+  const handleCheckedItemsChange = useCallback(
+    (checkedPages: IPageForTreeItem[]) => {
+      // Get current checked page IDs (with /* suffix paths)
+      const currentCheckedPaths = new Set(
+        checkedPages
+          .filter((page) => isSelectablePage(page) && page.path != null)
+          .map((page) => toPagePathGlob(page.path as string)),
+      );
+
+      // Get currently selected page paths
+      const currentSelectedPaths = new Set(selectedPages.keys());
+
+      // Add newly checked pages
+      checkedPages.forEach((page) => {
+        if (!isSelectablePage(page) || page.path == null) {
+          return;
+        }
+        const pagePathWithGlob = toPagePathGlob(page.path);
+        if (!currentSelectedPaths.has(pagePathWithGlob)) {
+          const clonedPage = { ...page, path: pagePathWithGlob };
+          addPage(clonedPage as SelectablePage);
+        }
+      });
+
+      // Remove unchecked pages
+      selectedPagesArray.forEach((page) => {
+        if (page.path != null && !currentCheckedPaths.has(page.path)) {
+          removePage(page);
+        }
+      });
+    },
+    [selectedPages, selectedPagesArray, addPage, removePage],
+  );
+
+  return {
+    selectedPages,
+    selectedPagesArray,
+    initialCheckedItems,
+    handleCheckedItemsChange,
+    addPage,
+    removePage,
+  };
+};