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

refactor: streamline selected pages handling and improve type usage across components

Shun Miyazawa 8 месяцев назад
Родитель
Сommit
166b2ce951

+ 8 - 26
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditPages.tsx

@@ -1,10 +1,9 @@
-import React, { useCallback, useMemo, type JSX } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import type { IPageHasId } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 import { ModalBody } from 'reactstrap';
 
-import type { IPageForItem } from '~/interfaces/page';
 import { useLimitLearnablePageCountPerAssistant } from '~/stores-universal/context';
 
 import type { SelectedPage } from '../../../../interfaces/selected-page';
@@ -13,17 +12,8 @@ import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
 import { PageSelectionMethodButtons } from './PageSelectionMethodButtons'; // Importing for side effects, if needed
 import { SelectablePagePageList } from './SelectablePagePageList';
 
-// 後で消す
-const isIPageHasId = (value?: IPageForItem): value is IPageHasId => {
-  if (value == null) {
-    return false;
-  }
-  return value.path != null;
-};
-
 type Props = {
   selectedPages: SelectedPage[];
-  onSelect: (page: IPageForItem, isIncludeSubPage: boolean) => void;
   onRemove: (pageId: string) => void;
 }
 
@@ -31,13 +21,7 @@ export const AiAssistantManagementEditPages = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { data: limitLearnablePageCountPerAssistant } = useLimitLearnablePageCountPerAssistant();
 
-  const { selectedPages, onSelect, onRemove } = props;
-
-  const pages = useMemo(() => {
-    return selectedPages
-      .map(selectedPageData => selectedPageData.page)
-      .filter(isIPageHasId);
-  }, [selectedPages]);
+  const { selectedPages, onRemove } = props;
 
   const removePageHandler = useCallback((page: IPageHasId) => {
     onRemove(page.path);
@@ -59,14 +43,12 @@ export const AiAssistantManagementEditPages = (props: Props): JSX.Element => {
             <PageSelectionMethodButtons />
           </div>
 
-          <div className="">
-            <SelectablePagePageList
-              method="delete"
-              methodButtonPosition="right"
-              pages={pages}
-              onClickMethodButton={removePageHandler}
-            />
-          </div>
+          <SelectablePagePageList
+            method="delete"
+            methodButtonPosition="right"
+            pages={selectedPages}
+            onClickMethodButton={removePageHandler}
+          />
         </div>
       </ModalBody>
     </>

+ 1 - 3
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHome.tsx

@@ -58,9 +58,7 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
 
   const totalSelectedPageCount = useMemo(() => {
     return selectedPages.reduce((total, selectedPage) => {
-      const descendantCount = selectedPage.isIncludeSubPage
-        ? selectedPage.page.descendantCount ?? 0
-        : 0;
+      const descendantCount = selectedPage.descendantCount ?? 0;
       const pageCountWithDescendants = descendantCount + 1;
       return total + pageCountWithDescendants;
     }, 0);

+ 24 - 16
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementKeywordSearch.tsx

@@ -12,6 +12,7 @@ import {
 
 import { useSWRxSearch } from '~/stores/search';
 
+import type { SelectedPage } from '../../../../interfaces/selected-page';
 import { useSelectedPages } from '../../../services/use-selected-pages';
 import {
   useAiAssistantManagementModal, AiAssistantManagementModalPageMode,
@@ -34,11 +35,18 @@ const isSelectedSearchKeyword = (value: unknown): value is SelectedSearchKeyword
 };
 
 
-export const AiAssistantKeywordSearch = (props: { updateBaseSelectedPages: (pages: IPageHasId[]) => void}): JSX.Element => {
-  const { updateBaseSelectedPages } = props;
+type Props = {
+  baseSelectedPages: SelectedPage[],
+  updateBaseSelectedPages: (pages: SelectedPage[]) => void;
+}
+
+export const AiAssistantKeywordSearch = (props: Props): JSX.Element => {
+  const { baseSelectedPages, updateBaseSelectedPages } = props;
 
   const [selectedSearchKeywords, setSelectedSearchKeywords] = useState<Array<SelectedSearchKeyword>>([]);
-  const { selectedPages, addPageHandler, removePageHandler } = useSelectedPages();
+  const {
+    selectedPages, addPage, removePage, clearPages,
+  } = useSelectedPages(baseSelectedPages);
 
   const joinedSelectedSearchKeywords = useMemo(() => {
     return selectedSearchKeywords.map(item => item.label).join(' ');
@@ -60,17 +68,16 @@ export const AiAssistantKeywordSearch = (props: { updateBaseSelectedPages: (page
 
     const pages = searchResult.data.map(item => item.data);
     return pages.map((page) => {
-      if (page.path === '/') {
-        page.path = '/*';
+      const newPage = { ...page };
+      if (newPage.path === '/') {
+        newPage.path = '/*';
+        return newPage;
       }
-
-      if (!isGlobPatternPath(page.path)) {
-        page.path = `${page.path}/*`;
+      if (!isGlobPatternPath(newPage.path)) {
+        newPage.path = `${newPage.path}/*`;
       }
-
-      return page;
+      return newPage;
     });
-
   }, [searchResult]);
 
   const shownSearchResult = useMemo(() => {
@@ -125,8 +132,9 @@ export const AiAssistantKeywordSearch = (props: { updateBaseSelectedPages: (page
 
   const nextButtonClickHandler = useCallback(() => {
     updateBaseSelectedPages(Array.from(selectedPages.values()));
-    changePageMode(AiAssistantManagementModalPageMode.HOME);
-  }, [changePageMode, selectedPages, updateBaseSelectedPages]);
+    changePageMode(isNewAiAssistant ? AiAssistantManagementModalPageMode.HOME : AiAssistantManagementModalPageMode.PAGES);
+    clearPages();
+  }, [changePageMode, clearPages, isNewAiAssistant, selectedPages, updateBaseSelectedPages]);
 
   return (
     <div className={moduleClass}>
@@ -168,8 +176,8 @@ export const AiAssistantKeywordSearch = (props: { updateBaseSelectedPages: (page
               <SelectablePagePageList
                 pages={pagesWithGlobPath ?? []}
                 method="add"
-                onClickMethodButton={addPageHandler}
-                disablePageIds={Array.from(selectedPages.keys())}
+                onClickMethodButton={addPage}
+                disablePagePaths={Array.from(selectedPages.values()).map(page => page.path)}
               />
             </div>
           </>
@@ -183,7 +191,7 @@ export const AiAssistantKeywordSearch = (props: { updateBaseSelectedPages: (page
           <SelectablePagePageList
             pages={Array.from(selectedPages.values())}
             method="remove"
-            onClickMethodButton={removePageHandler}
+            onClickMethodButton={removePage}
           />
           <label className="form-text text-muted mt-2">
             {t('modal_ai_assistant.can_add_later')}

+ 13 - 36
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.tsx

@@ -13,7 +13,7 @@ import { Modal, TabContent, TabPane } from 'reactstrap';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { UpsertAiAssistantData } from '~/features/openai/interfaces/ai-assistant';
 import { AiAssistantAccessScope, AiAssistantShareScope } from '~/features/openai/interfaces/ai-assistant';
-import type { IPagePathWithDescendantCount, IPageForItem } from '~/interfaces/page';
+import type { IPagePathWithDescendantCount } from '~/interfaces/page';
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import { useSWRxPagePathsWithDescendantCount } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
@@ -58,12 +58,11 @@ const convertToPopulatedGrantedGroups = (selectedGroups: IGrantedGroup[]): Popul
 
 const convertToSelectedPages = (pagePathPatterns: string[], pagePathsWithDescendantCount: IPagePathWithDescendantCount[]): SelectedPage[] => {
   return pagePathPatterns.map((pagePathPattern) => {
-    const isIncludeSubPage = pagePathPattern.endsWith('/*');
-    const path = isIncludeSubPage ? pagePathPattern.slice(0, -2) : pagePathPattern;
-    const page = pagePathsWithDescendantCount.find(page => page.path === path);
+    const pathWithoutGlob = isGlobPatternPath(pagePathPattern) ? pagePathPattern.slice(0, -2) : pagePathPattern;
+    const page = pagePathsWithDescendantCount.find(p => p.path === pathWithoutGlob);
     return {
-      page: page ?? { path },
-      isIncludeSubPage,
+      ...page,
+      path: pagePathPattern,
     };
   });
 };
@@ -121,23 +120,8 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   /*
   *  For AiAssistantManagementKeywordSearch & AiAssistantManagementPageTreeSelection methods
   */
-  const selectPageHandlerForKeywordSearchOrPageTreeSelection = useCallback((pages: IPageHasId[]) => {
-    if (pages.length === 0) {
-      return;
-    }
-
-    const convertedSelectedPages = pages.map((page) => {
-      const pagePath = page.path;
-      page.path = removeGlobPath([pagePath])[0];
-
-      return {
-        page,
-        // Determine whether to include subordinate pages from path
-        isIncludeSubPage: isGlobPatternPath(pagePath),
-      };
-    });
-
-    setSelectedPages(convertedSelectedPages);
+  const selectPageHandler = useCallback((pages: IPageHasId[]) => {
+    setSelectedPages(pages);
   }, []);
 
 
@@ -155,8 +139,7 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   const upsertAiAssistantHandler = useCallback(async() => {
     try {
       const pagePathPatterns = selectedPages
-        .map(selectedPage => (selectedPage.isIncludeSubPage ? `${selectedPage.page.path}/*` : selectedPage.page.path))
-        .filter((path): path is string => path !== undefined && path !== null);
+        .map(selectedPage => selectedPage.path);
 
       const grantedGroupsForShareScope = selectedShareScope === AiAssistantShareScope.GROUPS
         ? convertToGrantedGroups(selectedUserGroupsForShareScope)
@@ -241,15 +224,8 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   /*
   *  For AiAssistantManagementEditPages methods
   */
-  const selectPageHandler = useCallback((page: IPageForItem, isIncludeSubPage: boolean) => {
-    const selectedPageIds = selectedPages.map(selectedPage => selectedPage.page.path);
-    if (page.path != null && !selectedPageIds.includes(page.path)) {
-      setSelectedPages([...selectedPages, { page, isIncludeSubPage }]);
-    }
-  }, [selectedPages]);
-
   const removePageHandler = useCallback((pagePath: string) => {
-    setSelectedPages(selectedPages.filter(selectedPage => selectedPage.page.path !== pagePath));
+    setSelectedPages(selectedPages.filter(selectedPage => selectedPage.path !== pagePath));
   }, [selectedPages]);
 
 
@@ -273,13 +249,15 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
 
         <TabPane tabId={AiAssistantManagementModalPageMode.KEYWORD_SEARCH}>
           <AiAssistantKeywordSearch
-            updateBaseSelectedPages={selectPageHandlerForKeywordSearchOrPageTreeSelection}
+            baseSelectedPages={selectedPages}
+            updateBaseSelectedPages={selectPageHandler}
           />
         </TabPane>
 
         <TabPane tabId={AiAssistantManagementModalPageMode.PAGE_TREE_SELECTION}>
           <AiAssistantManagementPageTreeSelection
-            updateBaseSelectedPages={selectPageHandlerForKeywordSearchOrPageTreeSelection}
+            baseSelectedPages={selectedPages}
+            updateBaseSelectedPages={selectPageHandler}
           />
         </TabPane>
 
@@ -316,7 +294,6 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
         <TabPane tabId={AiAssistantManagementModalPageMode.PAGES}>
           <AiAssistantManagementEditPages
             selectedPages={selectedPages}
-            onSelect={selectPageHandler}
             onRemove={removePageHandler}
           />
         </TabPane>

+ 18 - 21
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageTreeSelection.tsx

@@ -15,6 +15,7 @@ import { TreeItemLayout } from '~/client/components/TreeItem';
 import type { IPageForItem } from '~/interfaces/page';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
 
+import { type SelectedPage, isSelectedPage } from '../../../../interfaces/selected-page';
 import { useSelectedPages } from '../../../services/use-selected-pages';
 import { AiAssistantManagementModalPageMode, useAiAssistantManagementModal } from '../../../stores/ai-assistant';
 
@@ -25,22 +26,14 @@ import styles from './AiAssistantManagementPageTreeSelection.module.scss';
 
 const moduleClass = styles['grw-ai-assistant-management-page-tree-selection'] ?? '';
 
-export const isIPageHasId = (value?: IPageForItem): value is IPageHasId => {
-  if (value == null) {
-    return false;
-  }
-  return value._id != null && value.path != null;
-};
-
-
-const SelectablePageTree = memo((props: { onClickAddPageButton: (page: IPageHasId) => void }) => {
+const SelectablePageTree = memo((props: { onClickAddPageButton: (page: SelectedPage) => void }) => {
   const { onClickAddPageButton } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
   const pageTreeItemClickHandler = useCallback((page: IPageForItem) => {
-    if (!isIPageHasId(page)) {
+    if (!isSelectedPage(page)) {
       return;
     }
 
@@ -88,21 +81,24 @@ const SelectablePageTree = memo((props: { onClickAddPageButton: (page: IPageHasI
   );
 });
 
+type Props = {
+  baseSelectedPages: SelectedPage[],
+  updateBaseSelectedPages: (pages: SelectedPage[]) => void;
+}
 
-export const AiAssistantManagementPageTreeSelection = (props: { updateBaseSelectedPages: (pages: IPageHasId[]) => void}): JSX.Element => {
-  const { updateBaseSelectedPages } = props;
+export const AiAssistantManagementPageTreeSelection = (props: Props): JSX.Element => {
+  const { baseSelectedPages, updateBaseSelectedPages } = props;
 
   const { t } = useTranslation();
   const { data: aiAssistantManagementModalData, changePageMode } = useAiAssistantManagementModal();
   const isNewAiAssistant = aiAssistantManagementModalData?.aiAssistantData == null;
 
-  const { selectedPages, addPageHandler, removePageHandler } = useSelectedPages();
+  const {
+    selectedPages, addPage, removePage, clearPages,
+  } = useSelectedPages(baseSelectedPages);
 
   // SelectedPages will include subordinate pages by default
-  const pagesWithGlobPath = useMemo((): IPageHasId[] | undefined => {
-    if (selectedPages.size === 0) {
-      return;
-    }
+  const pagesWithGlobPath = useMemo(() => {
     return Array.from(selectedPages.values()).map((page) => {
       if (page.path === '/') {
         page.path = '/*';
@@ -118,8 +114,9 @@ export const AiAssistantManagementPageTreeSelection = (props: { updateBaseSelect
 
   const nextButtonClickHandler = useCallback(() => {
     updateBaseSelectedPages(Array.from(selectedPages.values()));
-    changePageMode(AiAssistantManagementModalPageMode.HOME);
-  }, [changePageMode, selectedPages, updateBaseSelectedPages]);
+    changePageMode(isNewAiAssistant ? AiAssistantManagementModalPageMode.HOME : AiAssistantManagementModalPageMode.PAGES);
+    clearPages();
+  }, [changePageMode, clearPages, isNewAiAssistant, selectedPages, updateBaseSelectedPages]);
 
   return (
     <div className={moduleClass}>
@@ -136,7 +133,7 @@ export const AiAssistantManagementPageTreeSelection = (props: { updateBaseSelect
 
         <Suspense fallback={<ItemsTreeContentSkeleton />}>
           <div className="px-4">
-            <SelectablePageTree onClickAddPageButton={addPageHandler} />
+            <SelectablePageTree onClickAddPageButton={addPage} />
           </div>
         </Suspense>
 
@@ -149,7 +146,7 @@ export const AiAssistantManagementPageTreeSelection = (props: { updateBaseSelect
             method="remove"
             methodButtonPosition="right"
             pages={pagesWithGlobPath ?? []}
-            onClickMethodButton={removePageHandler}
+            onClickMethodButton={removePage}
           />
           <label className="form-text text-muted mt-2">
             {t('modal_ai_assistant.can_add_later')}

+ 9 - 8
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectablePagePageList.tsx

@@ -1,18 +1,19 @@
 import React, { useMemo } from 'react';
 
-import type { IPageHasId } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 
+import { type SelectedPage } from '../../../../interfaces/selected-page';
+
 import styles from './SelectablePagePageList.module.scss';
 
 const moduleClass = styles['selectable-page-page-list'] ?? '';
 
 type Props = {
-  pages: IPageHasId[],
+  pages: SelectedPage[],
   method: 'add' | 'remove' | 'delete'
   methodButtonPosition?: 'left' | 'right',
-  disablePageIds?: string[],
-  onClickMethodButton: (page: IPageHasId) => void,
+  disablePagePaths?: string[],
+  onClickMethodButton: (page: SelectedPage) => void,
 }
 
 export const SelectablePagePageList = (props: Props): JSX.Element => {
@@ -20,7 +21,7 @@ export const SelectablePagePageList = (props: Props): JSX.Element => {
     pages,
     method,
     methodButtonPosition = 'left',
-    disablePageIds,
+    disablePagePaths,
     onClickMethodButton,
   } = props;
 
@@ -52,12 +53,12 @@ export const SelectablePagePageList = (props: Props): JSX.Element => {
     }
   }, [method]);
 
-  const methodButton = (page: IPageHasId) => {
+  const methodButton = (page: SelectedPage) => {
     return (
       <button
         type="button"
         className={`btn border-0 ${methodButtonColor}`}
-        disabled={disablePageIds?.includes(page._id)}
+        disabled={disablePagePaths?.includes(page.path)}
         onClick={(e) => {
           e.stopPropagation();
           onClickMethodButton(page);
@@ -85,7 +86,7 @@ export const SelectablePagePageList = (props: Props): JSX.Element => {
       {pages.map((page) => {
         return (
           <button
-            key={page._id}
+            key={page.path}
             type="button"
             className="list-group-item border-0 list-group-item-action page-list-item d-flex align-items-center p-1 mb-2 rounded"
             onClick={(e) => {

+ 2 - 5
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectedPageList.tsx

@@ -15,16 +15,13 @@ const SelectedPageListBase: React.FC<SelectedPageListProps> = ({ selectedPages,
 
   return (
     <div className="mb-3">
-      {selectedPages.map(({ page, isIncludeSubPage }) => (
+      {selectedPages.map(page => (
         <div
           key={page.path}
           className="mb-2 d-flex justify-content-between align-items-center bg-body-tertiary rounded py-2 px-3"
         >
           <div className="d-flex align-items-center overflow-hidden text-body">
-            { isIncludeSubPage
-              ? <>{`${page.path}/*`}</>
-              : <>{page.path}</>
-            }
+            {page.path}
           </div>
           {onRemove != null && page.path != null && (
             <button

+ 2 - 2
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/ShareScopeWarningModal.tsx

@@ -48,8 +48,8 @@ export const ShareScopeWarningModal = (props: Props): JSX.Element => {
         <div className="mb-4">
           <p className="mb-2 text-secondary">{t('share_scope_warning_modal.selected_pages_label')}</p>
           {selectedPages.map(selectedPage => (
-            <code key={selectedPage.page.path}>
-              {selectedPage.page.path}
+            <code key={selectedPage.path}>
+              {selectedPage.path}
             </code>
           ))}
         </div>

+ 36 - 13
apps/app/src/features/openai/client/services/use-selected-pages.ts

@@ -1,35 +1,58 @@
-import { useState, useCallback } from 'react';
+import { useState, useCallback, useEffect } from 'react';
 
-import type { IPageHasId } from '@growi/core';
+// import type { IPageHasId } from '@growi/core';
+import type { SelectedPage } from '../../interfaces/selected-page';
 
 type UseSelectedPages = {
-  selectedPages: Map<string, IPageHasId>,
-  addPageHandler: (page: IPageHasId) => void,
-  removePageHandler: (page: IPageHasId) => void,
+  selectedPages: Map<string, SelectedPage>,
+  addPage: (page: SelectedPage) => void,
+  removePage: (page: SelectedPage) => void,
+  clearPages: () => void,
 }
 
-export const useSelectedPages = (): UseSelectedPages => {
-  const [selectedPages, setSelectedPages] = useState<Map<string, IPageHasId>>(new Map());
+export const useSelectedPages = (initialPages?: SelectedPage[]): UseSelectedPages => {
+  const [selectedPages, setSelectedPages] = useState<Map<string, SelectedPage>>(new Map());
 
-  const addPageHandler = useCallback((page: IPageHasId) => {
+  useEffect(() => {
+    if (initialPages) {
+      const initialMap = new Map<string, SelectedPage>();
+      initialPages.forEach((page) => {
+        if (page.path != null) {
+          initialMap.set(page.path, page);
+        }
+      });
+      setSelectedPages(initialMap);
+    }
+  }, [initialPages]);
+
+  const addPage = useCallback((page: SelectedPage) => {
     setSelectedPages((prev) => {
       const newMap = new Map(prev);
-      newMap.set(page._id, page);
+      if (page.path != null) {
+        newMap.set(page.path, page);
+      }
       return newMap;
     });
   }, []);
 
-  const removePageHandler = useCallback((page: IPageHasId) => {
+  const removePage = useCallback((page: SelectedPage) => {
     setSelectedPages((prev) => {
       const newMap = new Map(prev);
-      newMap.delete(page._id);
+      if (page.path != null) {
+        newMap.delete(page.path);
+      }
       return newMap;
     });
   }, []);
 
+  const clearPages = useCallback(() => {
+    setSelectedPages(new Map());
+  }, []);
+
   return {
     selectedPages,
-    addPageHandler,
-    removePageHandler,
+    addPage,
+    removePage,
+    clearPages,
   };
 };

+ 5 - 4
apps/app/src/features/openai/interfaces/selected-page.ts

@@ -2,7 +2,8 @@ import type { IPageHasId } from '@growi/core';
 
 import type { IPageForItem } from '~/interfaces/page';
 
-export type SelectedPage = {
-  page: IPageForItem | IPageHasId,
-  isIncludeSubPage: boolean,
-}
+export type SelectedPage = Partial<IPageHasId> & { path: string }
+
+export const isSelectedPage = (page: IPageForItem): page is SelectedPage => {
+  return page.path != null;
+};