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

refactor search hooks and components

Yuki Takei 6 месяцев назад
Родитель
Сommit
879efd81ca
24 измененных файлов с 650 добавлено и 441 удалено
  1. 3 2
      apps/app/src/client/components/Hotkeys/Subscribers/FocusToGlobalSearch.jsx
  2. 2 2
      apps/app/src/client/components/Navbar/GrowiNavbarBottom.tsx
  3. 2 2
      apps/app/src/client/components/PageControls/SearchButton.tsx
  4. 28 16
      apps/app/src/features/search/client/components/PrivateLegacyPages.tsx
  5. 10 6
      apps/app/src/features/search/client/components/SearchModal.tsx
  6. 13 9
      apps/app/src/features/search/client/components/SearchPage/OperateAllControl.tsx
  7. 0 0
      apps/app/src/features/search/client/components/SearchPage/SearchControl.module.scss
  8. 80 55
      apps/app/src/features/search/client/components/SearchPage/SearchControl.tsx
  9. 9 11
      apps/app/src/features/search/client/components/SearchPage/SearchModalTriggerinput.tsx
  10. 17 17
      apps/app/src/features/search/client/components/SearchPage/SearchOptionModal.tsx
  11. 0 0
      apps/app/src/features/search/client/components/SearchPage/SearchPage.module.scss
  12. 153 96
      apps/app/src/features/search/client/components/SearchPage/SearchPage.tsx
  13. 0 0
      apps/app/src/features/search/client/components/SearchPage/SearchPageBase.module.scss
  14. 103 76
      apps/app/src/features/search/client/components/SearchPage/SearchPageBase.tsx
  15. 0 0
      apps/app/src/features/search/client/components/SearchPage/SearchResultContent.module.scss
  16. 148 83
      apps/app/src/features/search/client/components/SearchPage/SearchResultContent.tsx
  17. 15 10
      apps/app/src/features/search/client/components/SearchPage/SearchResultList.tsx
  18. 0 0
      apps/app/src/features/search/client/components/SearchPage/SortControl.module.scss
  19. 17 15
      apps/app/src/features/search/client/components/SearchPage/SortControl.tsx
  20. 1 0
      apps/app/src/features/search/client/components/SearchPage/index.ts
  21. 47 0
      apps/app/src/features/search/client/states/modal/search.ts
  22. 0 39
      apps/app/src/features/search/client/stores/search.ts
  23. 1 1
      apps/app/src/pages/_private-legacy-pages/index.page.tsx
  24. 1 1
      apps/app/src/pages/_search/index.page.tsx

+ 3 - 2
apps/app/src/client/components/Hotkeys/Subscribers/FocusToGlobalSearch.jsx

@@ -1,12 +1,13 @@
 import { useEffect } from 'react';
 import { useEffect } from 'react';
 
 
-import { useSearchModal } from '~/features/search/client/stores/search';
+import { useSearchModalStatus, useSearchModalActions } from '~/features/search/client/states/modal/search';
 import { useIsEditable } from '~/states/page';
 import { useIsEditable } from '~/states/page';
 
 
 
 
 const FocusToGlobalSearch = (props) => {
 const FocusToGlobalSearch = (props) => {
   const isEditable = useIsEditable();
   const isEditable = useIsEditable();
-  const { data: searchModalData, open: openSearchModal } = useSearchModal();
+  const searchModalData = useSearchModalStatus();
+  const { open: openSearchModal } = useSearchModalActions();
 
 
   // setup effect
   // setup effect
   useEffect(() => {
   useEffect(() => {

+ 2 - 2
apps/app/src/client/components/Navbar/GrowiNavbarBottom.tsx

@@ -1,7 +1,7 @@
 import React, { useCallback, type JSX } from 'react';
 import React, { useCallback, type JSX } from 'react';
 
 
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
-import { useSearchModal } from '~/features/search/client/stores/search';
+import { useSearchModalActions } from '~/features/search/client/states/modal/search';
 import { useIsSearchPage } from '~/states/context';
 import { useIsSearchPage } from '~/states/context';
 import { useCurrentPagePath } from '~/states/page';
 import { useCurrentPagePath } from '~/states/page';
 import { usePageCreateModalActions } from '~/states/ui/modal/page-create';
 import { usePageCreateModalActions } from '~/states/ui/modal/page-create';
@@ -16,7 +16,7 @@ export const GrowiNavbarBottom = (): JSX.Element => {
   const { open: openCreateModal } = usePageCreateModalActions();
   const { open: openCreateModal } = usePageCreateModalActions();
   const currentPagePath = useCurrentPagePath();
   const currentPagePath = useCurrentPagePath();
   const isSearchPage = useIsSearchPage();
   const isSearchPage = useIsSearchPage();
-  const { open: openSearchModal } = useSearchModal();
+  const { open: openSearchModal } = useSearchModalActions();
 
 
   const searchButtonClickHandler = useCallback(() => {
   const searchButtonClickHandler = useCallback(() => {
     openSearchModal();
     openSearchModal();

+ 2 - 2
apps/app/src/client/components/PageControls/SearchButton.tsx

@@ -1,13 +1,13 @@
 import React, { useCallback, type JSX } from 'react';
 import React, { useCallback, type JSX } from 'react';
 
 
-import { useSearchModal } from '../../../features/search/client/stores/search';
+import { useSearchModalActions } from '~/features/search/client/states/modal/search';
 
 
 import styles from './SearchButton.module.scss';
 import styles from './SearchButton.module.scss';
 
 
 
 
 const SearchButton = (): JSX.Element => {
 const SearchButton = (): JSX.Element => {
 
 
-  const { open: openSearchModal } = useSearchModal();
+  const { open: openSearchModal } = useSearchModalActions();
 
 
   const searchButtonClickHandler = useCallback(() => {
   const searchButtonClickHandler = useCallback(() => {
     openSearchModal();
     openSearchModal();

+ 28 - 16
apps/app/src/client/components/PrivateLegacyPages.tsx → apps/app/src/features/search/client/components/PrivateLegacyPages.tsx

@@ -1,17 +1,31 @@
 import React, {
 import React, {
-  useCallback, useMemo, useRef, useState, useEffect, type JSX,
+  type JSX,
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
 } from 'react';
 } from 'react';
-
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
-  UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem, Modal, ModalHeader, ModalBody, ModalFooter,
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
+  Modal,
+  ModalBody,
+  ModalFooter,
+  ModalHeader,
+  UncontrolledButtonDropdown,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
+import { MenuItemType } from '~/client/components/Common/Dropdown/PageItemControl';
+import PaginationWrapper from '~/client/components/PaginationWrapper';
+import { PrivateLegacyPagesMigrationModal } from '~/client/components/PrivateLegacyPagesMigrationModal';
 import type { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
 import type { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
 import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
 import type { V5MigrationStatus } from '~/interfaces/page-listing-results';
 import type { V5MigrationStatus } from '~/interfaces/page-listing-results';
 import type { IFormattedSearchResult } from '~/interfaces/search';
 import type { IFormattedSearchResult } from '~/interfaces/search';
@@ -20,18 +34,15 @@ import { SocketEventName } from '~/interfaces/websocket';
 import { useIsAdmin } from '~/states/context';
 import { useIsAdmin } from '~/states/context';
 import type { ILegacyPrivatePage } from '~/states/ui/modal/private-legacy-pages-migration';
 import type { ILegacyPrivatePage } from '~/states/ui/modal/private-legacy-pages-migration';
 import { usePrivateLegacyPagesMigrationModalActions } from '~/states/ui/modal/private-legacy-pages-migration';
 import { usePrivateLegacyPagesMigrationModalActions } from '~/states/ui/modal/private-legacy-pages-migration';
-import { mutatePageTree, useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import {
 import {
-  useSWRxSearch,
-} from '~/stores/search';
+  mutatePageTree,
+  useSWRxV5MigrationStatus,
+} from '~/stores/page-listing';
+import { useSWRxSearch } from '~/stores/search';
 
 
-import { MenuItemType } from './Common/Dropdown/PageItemControl';
-import PaginationWrapper from './PaginationWrapper';
-import { PrivateLegacyPagesMigrationModal } from './PrivateLegacyPagesMigrationModal';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import SearchControl from './SearchPage/SearchControl';
 import SearchControl from './SearchPage/SearchControl';
-import type { IReturnSelectedPageIds } from './SearchPage/SearchPageBase';
-import { SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
+import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
 
 
 
 
 // TODO: replace with "customize:showPageLimitationS"
 // TODO: replace with "customize:showPageLimitationS"
@@ -39,7 +50,6 @@ const INITIAL_PAGING_SIZE = 20;
 
 
 const initQ = '/';
 const initQ = '/';
 
 
-
 /**
 /**
  * SearchResultListHead
  * SearchResultListHead
  */
  */
@@ -81,7 +91,7 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
           <h2 className="card-title text-success">{t('private_legacy_pages.nopages_title')}</h2>
           <h2 className="card-title text-success">{t('private_legacy_pages.nopages_title')}</h2>
           <p className="card-text">
           <p className="card-text">
             {t('private_legacy_pages.nopages_desc1')}<br />
             {t('private_legacy_pages.nopages_desc1')}<br />
-            {/* eslint-disable-next-line react/no-danger */}
+            {/** biome-ignore lint/security/noDangerouslySetInnerHtml: ignore */}
             <span dangerouslySetInnerHTML={{ __html: t('private_legacy_pages.detail_info') }}></span>
             <span dangerouslySetInnerHTML={{ __html: t('private_legacy_pages.detail_info') }}></span>
           </p>
           </p>
         </div>
         </div>
@@ -120,7 +130,7 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
           <h2 className="card-title text-warning">{t('private_legacy_pages.alert_title')}</h2>
           <h2 className="card-title text-warning">{t('private_legacy_pages.alert_title')}</h2>
           <p className="card-text">
           <p className="card-text">
             {t('private_legacy_pages.alert_desc1', { delete_all_selected_page: t('search_result.delete_all_selected_page') })}<br />
             {t('private_legacy_pages.alert_desc1', { delete_all_selected_page: t('search_result.delete_all_selected_page') })}<br />
-            {/* eslint-disable-next-line react/no-danger */}
+            {/** biome-ignore lint/security/noDangerouslySetInnerHtml: ignore */}
             <span dangerouslySetInnerHTML={{ __html: t('private_legacy_pages.detail_info') }}></span>
             <span dangerouslySetInnerHTML={{ __html: t('private_legacy_pages.detail_info') }}></span>
           </p>
           </p>
         </div>
         </div>
@@ -147,7 +157,7 @@ const ConvertByPathModal = React.memo((props: ConvertByPathModalProps): JSX.Elem
 
 
   useEffect(() => {
   useEffect(() => {
     setChecked(false);
     setChecked(false);
-  }, [props.isOpen]);
+  }, []);
 
 
   return (
   return (
     <Modal size="lg" isOpen={props.isOpen} toggle={props.close}>
     <Modal size="lg" isOpen={props.isOpen} toggle={props.close}>
@@ -399,6 +409,7 @@ const PrivateLegacyPages = (): JSX.Element => {
 
 
   const searchResultListHead = useMemo(() => {
   const searchResultListHead = useMemo(() => {
     if (data == null) {
     if (data == null) {
+      // biome-ignore lint/complexity/noUselessFragments: ignore
       return <></>;
       return <></>;
     }
     }
     return (
     return (
@@ -415,6 +426,7 @@ const PrivateLegacyPages = (): JSX.Element => {
   const searchPager = useMemo(() => {
   const searchPager = useMemo(() => {
     // when pager is not needed
     // when pager is not needed
     if (data == null || data.meta.hitsCount === data.meta.total) {
     if (data == null || data.meta.hitsCount === data.meta.total) {
+      // biome-ignore lint/complexity/noUselessFragments: ignore
       return <></>;
       return <></>;
     }
     }
 
 

+ 10 - 6
apps/app/src/features/search/client/components/SearchModal.tsx

@@ -1,15 +1,17 @@
+import { type JSX, useCallback, useEffect, useMemo, useState } from 'react';
+import { useRouter } from 'next/router';
 import Downshift, {
 import Downshift, {
   type DownshiftState,
   type DownshiftState,
   type StateChangeOptions,
   type StateChangeOptions,
 } from 'downshift';
 } from 'downshift';
-import { useRouter } from 'next/router';
-import { type JSX, useCallback, useEffect, useMemo, useState } from 'react';
 import { Modal, ModalBody } from 'reactstrap';
 import { Modal, ModalBody } from 'reactstrap';
 
 
 import { isIncludeAiMenthion, removeAiMenthion } from '../../utils/ai';
 import { isIncludeAiMenthion, removeAiMenthion } from '../../utils/ai';
 import type { DownshiftItem } from '../interfaces/downshift';
 import type { DownshiftItem } from '../interfaces/downshift';
-import { useSearchModal } from '../stores/search';
-
+import {
+  useSearchModalActions,
+  useSearchModalStatus,
+} from '../states/modal/search';
 import { SearchForm } from './SearchForm';
 import { SearchForm } from './SearchForm';
 import { SearchHelp } from './SearchHelp';
 import { SearchHelp } from './SearchHelp';
 import { SearchMethodMenuItem } from './SearchMethodMenuItem';
 import { SearchMethodMenuItem } from './SearchMethodMenuItem';
@@ -19,7 +21,8 @@ const SearchModalSubstance = (): JSX.Element => {
   const [searchKeyword, setSearchKeyword] = useState('');
   const [searchKeyword, setSearchKeyword] = useState('');
   const [isMenthionedToAi, setMenthionedToAi] = useState(false);
   const [isMenthionedToAi, setMenthionedToAi] = useState(false);
 
 
-  const { data: searchModalData, close: closeSearchModal } = useSearchModal();
+  const searchModalData = useSearchModalStatus();
+  const { close: closeSearchModal } = useSearchModalActions();
   const router = useRouter();
   const router = useRouter();
 
 
   const changeSearchTextHandler = useCallback((searchText: string) => {
   const changeSearchTextHandler = useCallback((searchText: string) => {
@@ -151,7 +154,8 @@ const SearchModalSubstance = (): JSX.Element => {
 };
 };
 
 
 const SearchModal = (): JSX.Element => {
 const SearchModal = (): JSX.Element => {
-  const { data: searchModalData, close: closeSearchModal } = useSearchModal();
+  const searchModalData = useSearchModalStatus();
+  const { close: closeSearchModal } = useSearchModalActions();
 
 
   // Early return for performance optimization
   // Early return for performance optimization
   if (!searchModalData?.isOpened) {
   if (!searchModalData?.isOpened) {

+ 13 - 9
apps/app/src/client/components/SearchPage/OperateAllControl.tsx → apps/app/src/features/search/client/components/SearchPage/OperateAllControl.tsx

@@ -1,20 +1,22 @@
 import type { ChangeEvent, ForwardRefRenderFunction, JSX } from 'react';
 import type { ChangeEvent, ForwardRefRenderFunction, JSX } from 'react';
 import React, { forwardRef, useImperativeHandle, useRef } from 'react';
 import React, { forwardRef, useImperativeHandle, useRef } from 'react';
-
 import { Input } from 'reactstrap';
 import { Input } from 'reactstrap';
 
 
 import type { ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
 import type { ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
 import type { IndeterminateInputElement } from '~/interfaces/indeterminate-input-elm';
 import type { IndeterminateInputElement } from '~/interfaces/indeterminate-input-elm';
 
 
 type Props = {
 type Props = {
-  inputId?: string,
-  inputClassName?: string,
-  isCheckboxDisabled?: boolean,
-  onCheckboxChanged?: (isChecked: boolean) => void,
-  children?: React.ReactNode,
-}
+  inputId?: string;
+  inputClassName?: string;
+  isCheckboxDisabled?: boolean;
+  onCheckboxChanged?: (isChecked: boolean) => void;
+  children?: React.ReactNode;
+};
 
 
-const OperateAllControlSubstance: ForwardRefRenderFunction<ISelectableAndIndeterminatable, Props> = (props: Props, ref): JSX.Element => {
+const OperateAllControlSubstance: ForwardRefRenderFunction<
+  ISelectableAndIndeterminatable,
+  Props
+> = (props: Props, ref): JSX.Element => {
   const {
   const {
     inputId,
     inputId,
     inputClassName = '',
     inputClassName = '',
@@ -71,4 +73,6 @@ const OperateAllControlSubstance: ForwardRefRenderFunction<ISelectableAndIndeter
   );
   );
 };
 };
 
 
-export const OperateAllControl = React.memo(forwardRef(OperateAllControlSubstance));
+export const OperateAllControl = React.memo(
+  forwardRef(OperateAllControlSubstance),
+);

+ 0 - 0
apps/app/src/client/components/SearchPage/SearchControl.module.scss → apps/app/src/features/search/client/components/SearchPage/SearchControl.module.scss


+ 80 - 55
apps/app/src/client/components/SearchPage/SearchControl.tsx → apps/app/src/features/search/client/components/SearchPage/SearchControl.tsx

@@ -1,7 +1,4 @@
-import React, {
-  useCallback, useEffect, useState, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { Collapse } from 'reactstrap';
 import { Collapse } from 'reactstrap';
 
 
@@ -15,20 +12,22 @@ import SortControl from './SortControl';
 import styles from './SearchControl.module.scss';
 import styles from './SearchControl.module.scss';
 
 
 type Props = {
 type Props = {
-  isEnableSort: boolean,
-  isEnableFilter: boolean,
-  initialSearchConditions: Partial<ISearchConditions>,
+  isEnableSort: boolean;
+  isEnableFilter: boolean;
+  initialSearchConditions: Partial<ISearchConditions>;
 
 
-  onSearchInvoked?: (keyword: string, configurations: Partial<ISearchConfigurations>) => void,
+  onSearchInvoked?: (
+    keyword: string,
+    configurations: Partial<ISearchConfigurations>,
+  ) => void;
 
 
-  extraControls: React.ReactNode,
+  extraControls: React.ReactNode;
 
 
-  collapseContents?: React.ReactNode,
-  isCollapsed?: boolean,
-}
+  collapseContents?: React.ReactNode;
+  isCollapsed?: boolean;
+};
 
 
 const SearchControl = React.memo((props: Props): JSX.Element => {
 const SearchControl = React.memo((props: Props): JSX.Element => {
-
   const {
   const {
     isEnableSort,
     isEnableSort,
     isEnableFilter,
     isEnableFilter,
@@ -42,38 +41,65 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
   const keywordOnInit = initialSearchConditions.keyword ?? '';
   const keywordOnInit = initialSearchConditions.keyword ?? '';
 
 
   const [keyword, setKeyword] = useState(keywordOnInit);
   const [keyword, setKeyword] = useState(keywordOnInit);
-  const [sort, setSort] = useState<SORT_AXIS>(initialSearchConditions.sort ?? SORT_AXIS.RELATION_SCORE);
-  const [order, setOrder] = useState<SORT_ORDER>(initialSearchConditions.order ?? SORT_ORDER.DESC);
-  const [includeUserPages, setIncludeUserPages] = useState(initialSearchConditions.includeUserPages ?? false);
-  const [includeTrashPages, setIncludeTrashPages] = useState(initialSearchConditions.includeTrashPages ?? false);
-  const [isFileterOptionModalShown, setIsFileterOptionModalShown] = useState(false);
+  const [sort, setSort] = useState<SORT_AXIS>(
+    initialSearchConditions.sort ?? SORT_AXIS.RELATION_SCORE,
+  );
+  const [order, setOrder] = useState<SORT_ORDER>(
+    initialSearchConditions.order ?? SORT_ORDER.DESC,
+  );
+  const [includeUserPages, setIncludeUserPages] = useState(
+    initialSearchConditions.includeUserPages ?? false,
+  );
+  const [includeTrashPages, setIncludeTrashPages] = useState(
+    initialSearchConditions.includeTrashPages ?? false,
+  );
+  const [isFileterOptionModalShown, setIsFileterOptionModalShown] =
+    useState(false);
 
 
   const { t } = useTranslation('');
   const { t } = useTranslation('');
 
 
-  const changeSortHandler = useCallback((nextSort: SORT_AXIS, nextOrder: SORT_ORDER) => {
-    setSort(nextSort);
-    setOrder(nextOrder);
-
-    onSearchInvoked?.(keyword, {
-      sort: nextSort, order: nextOrder, includeUserPages, includeTrashPages,
-    });
-  }, [includeTrashPages, includeUserPages, keyword, onSearchInvoked]);
-
-  const changeIncludeUserPagesHandler = useCallback((include: boolean) => {
-    setIncludeUserPages(include);
-
-    onSearchInvoked?.(keyword, {
-      sort, order, includeUserPages: include, includeTrashPages,
-    });
-  }, [includeTrashPages, keyword, onSearchInvoked, order, sort]);
+  const changeSortHandler = useCallback(
+    (nextSort: SORT_AXIS, nextOrder: SORT_ORDER) => {
+      setSort(nextSort);
+      setOrder(nextOrder);
+
+      onSearchInvoked?.(keyword, {
+        sort: nextSort,
+        order: nextOrder,
+        includeUserPages,
+        includeTrashPages,
+      });
+    },
+    [includeTrashPages, includeUserPages, keyword, onSearchInvoked],
+  );
 
 
-  const changeIncludeTrashPagesHandler = useCallback((include: boolean) => {
-    setIncludeTrashPages(include);
+  const changeIncludeUserPagesHandler = useCallback(
+    (include: boolean) => {
+      setIncludeUserPages(include);
+
+      onSearchInvoked?.(keyword, {
+        sort,
+        order,
+        includeUserPages: include,
+        includeTrashPages,
+      });
+    },
+    [includeTrashPages, keyword, onSearchInvoked, order, sort],
+  );
 
 
-    onSearchInvoked?.(keyword, {
-      sort, order, includeUserPages, includeTrashPages: include,
-    });
-  }, [includeUserPages, keyword, onSearchInvoked, order, sort]);
+  const changeIncludeTrashPagesHandler = useCallback(
+    (include: boolean) => {
+      setIncludeTrashPages(include);
+
+      onSearchInvoked?.(keyword, {
+        sort,
+        order,
+        includeUserPages,
+        includeTrashPages: include,
+      });
+    },
+    [includeUserPages, keyword, onSearchInvoked, order, sort],
+  );
 
 
   useEffect(() => {
   useEffect(() => {
     setKeyword(keywordOnInit);
     setKeyword(keywordOnInit);
@@ -83,9 +109,7 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
     <div className="shadow-sm">
     <div className="shadow-sm">
       <div className="grw-search-page-nav d-flex py-3 align-items-center">
       <div className="grw-search-page-nav d-flex py-3 align-items-center">
         <div className="flex-grow-1 mx-4">
         <div className="flex-grow-1 mx-4">
-          <SearchModalTriggerinput
-            keywordOnInit={keyword}
-          />
+          <SearchModalTriggerinput keywordOnInit={keyword} />
         </div>
         </div>
       </div>
       </div>
       {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
       {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
@@ -109,9 +133,7 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
                 className="btn"
                 className="btn"
                 onClick={() => setIsFileterOptionModalShown(true)}
                 onClick={() => setIsFileterOptionModalShown(true)}
               >
               >
-                <span className="material-symbols-outlined">
-                  tune
-                </span>
+                <span className="material-symbols-outlined">tune</span>
               </button>
               </button>
             </div>
             </div>
             <div className="d-none d-lg-flex align-items-center search-control-include-options">
             <div className="d-none d-lg-flex align-items-center search-control-include-options">
@@ -122,7 +144,9 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
                     type="checkbox"
                     type="checkbox"
                     id="flexCheckDefault"
                     id="flexCheckDefault"
                     defaultChecked={includeUserPages}
                     defaultChecked={includeUserPages}
-                    onChange={e => changeIncludeUserPagesHandler(e.target.checked)}
+                    onChange={(e) =>
+                      changeIncludeUserPagesHandler(e.target.checked)
+                    }
                   />
                   />
                   <label
                   <label
                     className="form-label form-check-label mb-0 d-flex align-items-center text-secondary with-no-font-weight"
                     className="form-label form-check-label mb-0 d-flex align-items-center text-secondary with-no-font-weight"
@@ -139,13 +163,17 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
                     type="checkbox"
                     type="checkbox"
                     id="flexCheckChecked"
                     id="flexCheckChecked"
                     checked={includeTrashPages}
                     checked={includeTrashPages}
-                    onChange={e => changeIncludeTrashPagesHandler(e.target.checked)}
+                    onChange={(e) =>
+                      changeIncludeTrashPagesHandler(e.target.checked)
+                    }
                   />
                   />
                   <label
                   <label
                     className="form-label form-check-label mb-0 d-flex align-items-center text-secondary with-no-font-weight"
                     className="form-label form-check-label mb-0 d-flex align-items-center text-secondary with-no-font-weight"
                     htmlFor="flexCheckChecked"
                     htmlFor="flexCheckChecked"
                   >
                   >
-                    {t('Include Subordinated Target Page', { target: '/trash' })}
+                    {t('Include Subordinated Target Page', {
+                      target: '/trash',
+                    })}
                   </label>
                   </label>
                 </div>
                 </div>
               </div>
               </div>
@@ -156,11 +184,9 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
         {extraControls}
         {extraControls}
       </div>
       </div>
 
 
-      { collapseContents != null && (
-        <Collapse isOpen={isCollapsed}>
-          {collapseContents}
-        </Collapse>
-      ) }
+      {collapseContents != null && (
+        <Collapse isOpen={isCollapsed}>{collapseContents}</Collapse>
+      )}
 
 
       <SearchOptionModal
       <SearchOptionModal
         isOpen={isFileterOptionModalShown || false}
         isOpen={isFileterOptionModalShown || false}
@@ -170,7 +196,6 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
         onIncludeUserPagesSwitched={setIncludeUserPages}
         onIncludeUserPagesSwitched={setIncludeUserPages}
         onIncludeTrashPagesSwitched={setIncludeTrashPages}
         onIncludeTrashPagesSwitched={setIncludeTrashPages}
       />
       />
-
     </div>
     </div>
   );
   );
 });
 });

+ 9 - 11
apps/app/src/client/components/SearchPage/SearchModalTriggerinput.tsx → apps/app/src/features/search/client/components/SearchPage/SearchModalTriggerinput.tsx

@@ -1,17 +1,16 @@
-import React, {
-  useCallback,
-} from 'react';
+import type React from 'react';
+import { useCallback } from 'react';
 
 
-import { useSearchModal } from '../../../features/search/client/stores/search';
+import { useSearchModalActions } from '../../states/modal/search';
 
 
 type Props = {
 type Props = {
-  keywordOnInit: string,
+  keywordOnInit: string;
 };
 };
 
 
 export const SearchModalTriggerinput: React.FC<Props> = (props: Props) => {
 export const SearchModalTriggerinput: React.FC<Props> = (props: Props) => {
   const { keywordOnInit } = props;
   const { keywordOnInit } = props;
 
 
-  const { open: openSearchModal } = useSearchModal();
+  const { open: openSearchModal } = useSearchModalActions();
 
 
   const inputClickHandler = useCallback(() => {
   const inputClickHandler = useCallback(() => {
     openSearchModal(keywordOnInit);
     openSearchModal(keywordOnInit);
@@ -19,11 +18,10 @@ export const SearchModalTriggerinput: React.FC<Props> = (props: Props) => {
 
 
   return (
   return (
     <div className="d-flex align-items-center">
     <div className="d-flex align-items-center">
-      <span className="text-secondary material-symbols-outlined fs-4 me-2">search</span>
-      <form
-        className="w-100 position-relative"
-        onClick={inputClickHandler}
-      >
+      <span className="text-secondary material-symbols-outlined fs-4 me-2">
+        search
+      </span>
+      <form className="w-100 position-relative" onClick={inputClickHandler}>
         <input
         <input
           className="form-control"
           className="form-control"
           type="input"
           type="input"

+ 17 - 17
apps/app/src/client/components/SearchPage/SearchOptionModal.tsx → apps/app/src/features/search/client/components/SearchPage/SearchOptionModal.tsx

@@ -1,27 +1,23 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
-import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import {
-  Modal, ModalHeader, ModalBody,
-} from 'reactstrap';
-
+import { Modal, ModalBody, ModalHeader } from 'reactstrap';
 
 
 type Props = {
 type Props = {
-  isOpen: boolean,
-  includeUserPages: boolean,
-  includeTrashPages: boolean,
-  onClose?: () => void,
-  onIncludeUserPagesSwitched?: (isChecked: boolean) => void,
-  onIncludeTrashPagesSwitched?: (isChecked: boolean) => void,
-}
+  isOpen: boolean;
+  includeUserPages: boolean;
+  includeTrashPages: boolean;
+  onClose?: () => void;
+  onIncludeUserPagesSwitched?: (isChecked: boolean) => void;
+  onIncludeTrashPagesSwitched?: (isChecked: boolean) => void;
+};
 
 
 const SearchOptionModal: FC<Props> = (props: Props) => {
 const SearchOptionModal: FC<Props> = (props: Props) => {
-
   const { t } = useTranslation('');
   const { t } = useTranslation('');
 
 
   const {
   const {
-    isOpen, includeUserPages, includeTrashPages,
+    isOpen,
+    includeUserPages,
+    includeTrashPages,
     onClose,
     onClose,
     onIncludeUserPagesSwitched,
     onIncludeUserPagesSwitched,
     onIncludeTrashPagesSwitched,
     onIncludeTrashPagesSwitched,
@@ -57,7 +53,9 @@ const SearchOptionModal: FC<Props> = (props: Props) => {
               <input
               <input
                 className="me-2"
                 className="me-2"
                 type="checkbox"
                 type="checkbox"
-                onChange={e => includeUserPagesChangeHandler(e.target.checked)}
+                onChange={(e) =>
+                  includeUserPagesChangeHandler(e.target.checked)
+                }
                 checked={includeUserPages}
                 checked={includeUserPages}
               />
               />
               {t('Include Subordinated Target Page', { target: '/user' })}
               {t('Include Subordinated Target Page', { target: '/user' })}
@@ -68,7 +66,9 @@ const SearchOptionModal: FC<Props> = (props: Props) => {
               <input
               <input
                 className="me-2"
                 className="me-2"
                 type="checkbox"
                 type="checkbox"
-                onChange={e => includeTrashPagesChangeHandler(e.target.checked)}
+                onChange={(e) =>
+                  includeTrashPagesChangeHandler(e.target.checked)
+                }
                 checked={includeTrashPages}
                 checked={includeTrashPages}
               />
               />
               {t('Include Subordinated Target Page', { target: '/trash' })}
               {t('Include Subordinated Target Page', { target: '/trash' })}

+ 0 - 0
apps/app/src/client/components/SearchPage.module.scss → apps/app/src/features/search/client/components/SearchPage/SearchPage.module.scss


+ 153 - 96
apps/app/src/client/components/SearchPage.tsx → apps/app/src/features/search/client/components/SearchPage/SearchPage.tsx

@@ -1,67 +1,83 @@
-import React, {
-  useCallback, useMemo, useRef, useState, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useMemo, useRef, useState } from 'react';
 import { useAtomValue } from 'jotai';
 import { useAtomValue } from 'jotai';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import type { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
+import { NotAvailableForGuest } from '~/client/components/NotAvailableForGuest';
+import { NotAvailableForReadOnlyUser } from '~/client/components/NotAvailableForReadOnlyUser';
+import PaginationWrapper from '~/client/components/PaginationWrapper';
+import type {
+  ISelectableAll,
+  ISelectableAndIndeterminatable,
+} from '~/client/interfaces/selectable-all';
 import { useKeywordManager } from '~/client/services/search-operation';
 import { useKeywordManager } from '~/client/services/search-operation';
 import type { IFormattedSearchResult } from '~/interfaces/search';
 import type { IFormattedSearchResult } from '~/interfaces/search';
 import { showPageLimitationLAtom } from '~/states/server-configurations';
 import { showPageLimitationLAtom } from '~/states/server-configurations';
-import { type ISearchConditions, type ISearchConfigurations, useSWRxSearch } from '~/stores/search';
-
-import { NotAvailableForGuest } from './NotAvailableForGuest';
-import { NotAvailableForReadOnlyUser } from './NotAvailableForReadOnlyUser';
-import PaginationWrapper from './PaginationWrapper';
-import { OperateAllControl } from './SearchPage/OperateAllControl';
-import SearchControl from './SearchPage/SearchControl';
-import { type IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
+import {
+  type ISearchConditions,
+  type ISearchConfigurations,
+  useSWRxSearch,
+} from '~/stores/search';
+
+import { OperateAllControl } from './OperateAllControl';
+import SearchControl from './SearchControl';
+import type { IReturnSelectedPageIds } from './SearchPageBase';
+import {
+  SearchPageBase,
+  usePageDeleteModalForBulkDeletion,
+} from './SearchPageBase';
 
 
 import styles from './SearchPage.module.scss';
 import styles from './SearchPage.module.scss';
 
 
 // TODO: replace with "customize:showPageLimitationS"
 // TODO: replace with "customize:showPageLimitationS"
 const INITIAL_PAGIONG_SIZE = 20;
 const INITIAL_PAGIONG_SIZE = 20;
 
 
-
 /**
 /**
  * SearchResultListHead
  * SearchResultListHead
  */
  */
 
 
 type SearchResultListHeadProps = {
 type SearchResultListHeadProps = {
-  searchResult: IFormattedSearchResult,
-  pagingSize: number,
-  onPagingSizeChanged: (size: number) => void,
-}
+  searchResult: IFormattedSearchResult;
+  pagingSize: number;
+  onPagingSizeChanged: (size: number) => void;
+};
 
 
-const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.Element => {
-  const { t } = useTranslation();
+const SearchResultListHead = React.memo(
+  (props: SearchResultListHeadProps): JSX.Element => {
+    const { t } = useTranslation();
 
 
-  const {
-    searchResult, // pagingSize, onPagingSizeChanged,
-  } = props;
+    const {
+      searchResult, // pagingSize, onPagingSizeChanged,
+    } = props;
 
 
-  const { took, total } = searchResult.meta;
+    const { took, total } = searchResult.meta;
 
 
-  if (total === 0) {
-    return (
-      <div className="d-flex justify-content-center h2 text-muted my-5">
-        0 {t('search_result.page_number_unit')}
-      </div>
-    );
-  }
+    if (total === 0) {
+      return (
+        <div className="d-flex justify-content-center h2 text-muted my-5">
+          0 {t('search_result.page_number_unit')}
+        </div>
+      );
+    }
 
 
-  return (
-    <div className="d-flex align-items-center justify-content-between">
-      <div className="text-nowrap">
-        <span className="ms-3 fw-bold">{total} {t('search_result.hit_number_unit', 'hit')}</span>
-        { took != null && (
-          // blackout 70px rectangle in VRT
-          (<span data-vrt-blackout className="ms-3 text-muted d-inline-block" style={{ minWidth: '70px' }}>({took}ms)</span>)
-        ) }
-      </div>
-      {/* TODO: infinite scroll for search result */}
-      {/* <div className="input-group flex-nowrap search-result-select-group ms-auto d-md-flex d-none">
+    return (
+      <div className="d-flex align-items-center justify-content-between">
+        <div className="text-nowrap">
+          <span className="ms-3 fw-bold">
+            {total} {t('search_result.hit_number_unit', 'hit')}
+          </span>
+          {took != null && (
+            // blackout 70px rectangle in VRT
+            <span
+              data-vrt-blackout
+              className="ms-3 text-muted d-inline-block"
+              style={{ minWidth: '70px' }}
+            >
+              ({took}ms)
+            </span>
+          )}
+        </div>
+        {/* TODO: infinite scroll for search result */}
+        {/* <div className="input-group flex-nowrap search-result-select-group ms-auto d-md-flex d-none">
         <div>
         <div>
           <label className="form-label input-group-text text-muted" htmlFor="inputGroupSelect01">{t('search_result.number_of_list_to_display')}</label>
           <label className="form-label input-group-text text-muted" htmlFor="inputGroupSelect01">{t('search_result.number_of_list_to_display')}</label>
         </div>
         </div>
@@ -76,13 +92,13 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
           })}
           })}
         </select>
         </select>
       </div> */}
       </div> */}
-    </div>
-  );
-});
+      </div>
+    );
+  },
+);
 
 
 SearchResultListHead.displayName = 'SearchResultListHead';
 SearchResultListHead.displayName = 'SearchResultListHead';
 
 
-
 export const SearchPage = (): JSX.Element => {
 export const SearchPage = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const showPageLimitationL = useAtomValue(showPageLimitationLAtom);
   const showPageLimitationL = useAtomValue(showPageLimitationLAtom);
@@ -90,13 +106,21 @@ export const SearchPage = (): JSX.Element => {
   const { data: keyword, pushState } = useKeywordManager();
   const { data: keyword, pushState } = useKeywordManager();
 
 
   const [offset, setOffset] = useState<number>(0);
   const [offset, setOffset] = useState<number>(0);
-  const [limit, setLimit] = useState<number>(showPageLimitationL ?? INITIAL_PAGIONG_SIZE);
-  const [configurationsByControl, setConfigurationsByControl] = useState<Partial<ISearchConfigurations>>({});
+  const [limit, setLimit] = useState<number>(
+    showPageLimitationL ?? INITIAL_PAGIONG_SIZE,
+  );
+  const [configurationsByControl, setConfigurationsByControl] = useState<
+    Partial<ISearchConfigurations>
+  >({});
   const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
   const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
   const [selectedCount, setSelectedCount] = useState(0);
   const [selectedCount, setSelectedCount] = useState(0);
 
 
-  const selectAllControlRef = useRef<ISelectableAndIndeterminatable|null>(null);
-  const searchPageBaseRef = useRef<ISelectableAll & IReturnSelectedPageIds|null>(null);
+  const selectAllControlRef = useRef<ISelectableAndIndeterminatable | null>(
+    null,
+  );
+  const searchPageBaseRef = useRef<
+    (ISelectableAll & IReturnSelectedPageIds) | null
+  >(null);
 
 
   const { data, conditions, mutate } = useSWRxSearch(keyword ?? '', null, {
   const { data, conditions, mutate } = useSWRxSearch(keyword ?? '', null, {
     ...configurationsByControl,
     ...configurationsByControl,
@@ -104,14 +128,17 @@ export const SearchPage = (): JSX.Element => {
     limit,
     limit,
   });
   });
 
 
-  const searchInvokedHandler = useCallback((newKeyword: string, newConfigurations: Partial<ISearchConfigurations>) => {
-    setOffset(0);
-    setConfigurationsByControl(newConfigurations);
+  const searchInvokedHandler = useCallback(
+    (newKeyword: string, newConfigurations: Partial<ISearchConfigurations>) => {
+      setOffset(0);
+      setConfigurationsByControl(newConfigurations);
 
 
-    pushState(newKeyword);
+      pushState(newKeyword);
 
 
-    mutate();
-  }, [mutate, pushState]);
+      mutate();
+    },
+    [mutate, pushState],
+  );
 
 
   const selectAllCheckboxChangedHandler = useCallback((isChecked: boolean) => {
   const selectAllCheckboxChangedHandler = useCallback((isChecked: boolean) => {
     const instance = searchPageBaseRef.current;
     const instance = searchPageBaseRef.current;
@@ -122,8 +149,7 @@ export const SearchPage = (): JSX.Element => {
 
 
     if (isChecked) {
     if (isChecked) {
       instance.selectAll();
       instance.selectAll();
-    }
-    else {
+    } else {
       instance.deselectAll();
       instance.deselectAll();
     }
     }
 
 
@@ -131,38 +157,45 @@ export const SearchPage = (): JSX.Element => {
     setSelectedCount(instance.getSelectedPageIds?.().size ?? 0);
     setSelectedCount(instance.getSelectedPageIds?.().size ?? 0);
   }, []);
   }, []);
 
 
-  const selectedPagesByCheckboxesChangedHandler = useCallback((selectedCount: number, totalCount: number) => {
-    const instance = selectAllControlRef.current;
-
-    if (instance == null) {
-      return;
-    }
-
-    if (selectedCount === 0) {
-      instance.deselect();
-    }
-    else if (selectedCount === totalCount) {
-      instance.select();
-    }
-    else {
-      setIsCollapsed(true);
-      instance.setIndeterminate();
-    }
-
-    // update selected count
-    setSelectedCount(selectedCount);
-  }, []);
+  const selectedPagesByCheckboxesChangedHandler = useCallback(
+    (selectedCount: number, totalCount: number) => {
+      const instance = selectAllControlRef.current;
+
+      if (instance == null) {
+        return;
+      }
+
+      if (selectedCount === 0) {
+        instance.deselect();
+      } else if (selectedCount === totalCount) {
+        instance.select();
+      } else {
+        setIsCollapsed(true);
+        instance.setIndeterminate();
+      }
+
+      // update selected count
+      setSelectedCount(selectedCount);
+    },
+    [],
+  );
 
 
-  const pagingSizeChangedHandler = useCallback((pagingSize: number) => {
-    setOffset(0);
-    setLimit(pagingSize);
-    mutate();
-  }, [mutate]);
+  const pagingSizeChangedHandler = useCallback(
+    (pagingSize: number) => {
+      setOffset(0);
+      setLimit(pagingSize);
+      mutate();
+    },
+    [mutate],
+  );
 
 
-  const pagingNumberChangedHandler = useCallback((activePage: number) => {
-    setOffset((activePage - 1) * limit);
-    mutate();
-  }, [limit, mutate]);
+  const pagingNumberChangedHandler = useCallback(
+    (activePage: number) => {
+      setOffset((activePage - 1) * limit);
+      mutate();
+    },
+    [limit, mutate],
+  );
 
 
   const initialSearchConditions: Partial<ISearchConditions> = useMemo(() => {
   const initialSearchConditions: Partial<ISearchConditions> = useMemo(() => {
     return {
     return {
@@ -172,7 +205,11 @@ export const SearchPage = (): JSX.Element => {
   }, [keyword]);
   }, [keyword]);
 
 
   // for bulk deletion
   // for bulk deletion
-  const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(data, searchPageBaseRef, () => mutate());
+  const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(
+    data,
+    searchPageBaseRef,
+    () => mutate(),
+  );
 
 
   const hitsCount = data?.meta.hitsCount;
   const hitsCount = data?.meta.hitsCount;
 
 
@@ -184,10 +221,16 @@ export const SearchPage = (): JSX.Element => {
             type="button"
             type="button"
             className={`${isCollapsed ? 'active' : ''} btn btn-muted-danger d-flex align-items-center ms-2`}
             className={`${isCollapsed ? 'active' : ''} btn btn-muted-danger d-flex align-items-center ms-2`}
             aria-expanded="false"
             aria-expanded="false"
-            onClick={() => { setIsCollapsed(!isCollapsed) }}
+            onClick={() => {
+              setIsCollapsed(!isCollapsed);
+            }}
           >
           >
             <span className="material-symbols-outlined fs-5">delete</span>
             <span className="material-symbols-outlined fs-5">delete</span>
-            <span className={`material-symbols-outlined me-1 ${isCollapsed ? 'rotate-180' : ''}`}>keyboard_arrow_down</span>
+            <span
+              className={`material-symbols-outlined me-1 ${isCollapsed ? 'rotate-180' : ''}`}
+            >
+              keyboard_arrow_down
+            </span>
           </button>
           </button>
         </NotAvailableForReadOnlyUser>
         </NotAvailableForReadOnlyUser>
       </NotAvailableForGuest>
       </NotAvailableForGuest>
@@ -222,14 +265,20 @@ export const SearchPage = (): JSX.Element => {
               disabled={selectedCount === 0}
               disabled={selectedCount === 0}
               onClick={deleteAllButtonClickedHandler}
               onClick={deleteAllButtonClickedHandler}
             >
             >
-              <span className="material-symbols-outlined fs-5">delete</span>{t('search_result.delete_selected_pages')}
+              <span className="material-symbols-outlined fs-5">delete</span>
+              {t('search_result.delete_selected_pages')}
             </button>
             </button>
           </div>
           </div>
         </NotAvailableForReadOnlyUser>
         </NotAvailableForReadOnlyUser>
       </NotAvailableForGuest>
       </NotAvailableForGuest>
     );
     );
-  }, [deleteAllButtonClickedHandler, hitsCount, selectAllCheckboxChangedHandler, selectedCount, t]);
-
+  }, [
+    deleteAllButtonClickedHandler,
+    hitsCount,
+    selectAllCheckboxChangedHandler,
+    selectedCount,
+    t,
+  ]);
 
 
   const searchControl = useMemo(() => {
   const searchControl = useMemo(() => {
     return (
     return (
@@ -243,7 +292,13 @@ export const SearchPage = (): JSX.Element => {
         isCollapsed={isCollapsed}
         isCollapsed={isCollapsed}
       />
       />
     );
     );
-  }, [extraControls, collapseContents, initialSearchConditions, isCollapsed, searchInvokedHandler]);
+  }, [
+    extraControls,
+    collapseContents,
+    initialSearchConditions,
+    isCollapsed,
+    searchInvokedHandler,
+  ]);
 
 
   const searchResultListHead = useMemo(() => {
   const searchResultListHead = useMemo(() => {
     if (data == null) {
     if (data == null) {
@@ -283,7 +338,9 @@ export const SearchPage = (): JSX.Element => {
       ref={searchPageBaseRef}
       ref={searchPageBaseRef}
       pages={data?.data}
       pages={data?.data}
       searchingKeyword={keyword}
       searchingKeyword={keyword}
-      onSelectedPagesByCheckboxesChanged={selectedPagesByCheckboxesChangedHandler}
+      onSelectedPagesByCheckboxesChanged={
+        selectedPagesByCheckboxesChangedHandler
+      }
       // Components
       // Components
       searchControl={searchControl}
       searchControl={searchControl}
       searchResultListHead={searchResultListHead}
       searchResultListHead={searchResultListHead}

+ 0 - 0
apps/app/src/client/components/SearchPage/SearchPageBase.module.scss → apps/app/src/features/search/client/components/SearchPage/SearchPageBase.module.scss


+ 103 - 76
apps/app/src/client/components/SearchPage/SearchPageBase.tsx → apps/app/src/features/search/client/components/SearchPage/SearchPageBase.tsx

@@ -1,24 +1,33 @@
+import type React from 'react';
 import type { ForwardRefRenderFunction, JSX } from 'react';
 import type { ForwardRefRenderFunction, JSX } from 'react';
-import React, {
-  forwardRef, useEffect, useImperativeHandle, useRef, useState,
+import {
+  forwardRef,
+  useEffect,
+  useImperativeHandle,
+  useRef,
+  useState,
 } from 'react';
 } from 'react';
-
+import dynamic from 'next/dynamic';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useAtomValue } from 'jotai';
 import { useAtomValue } from 'jotai';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import dynamic from 'next/dynamic';
 
 
+import type { ForceHideMenuItems } from '~/client/components/Common/Dropdown/PageItemControl';
 import type { ISelectableAll } from '~/client/interfaces/selectable-all';
 import type { ISelectableAll } from '~/client/interfaces/selectable-all';
 import { toastSuccess } from '~/client/util/toastr';
 import { toastSuccess } from '~/client/util/toastr';
-import type { IFormattedSearchResult, IPageWithSearchMeta } from '~/interfaces/search';
+import type {
+  IFormattedSearchResult,
+  IPageWithSearchMeta,
+} from '~/interfaces/search';
 import type { OnDeletedFunction } from '~/interfaces/ui';
 import type { OnDeletedFunction } from '~/interfaces/ui';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
-import { isSearchServiceConfiguredAtom, isSearchServiceReachableAtom } from '~/states/server-configurations';
+import {
+  isSearchServiceConfiguredAtom,
+  isSearchServiceReachableAtom,
+} from '~/states/server-configurations';
 import { usePageDeleteModalActions } from '~/states/ui/modal/page-delete';
 import { usePageDeleteModalActions } from '~/states/ui/modal/page-delete';
 import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 
 
-import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-
 // Do not import with next/dynamic
 // Do not import with next/dynamic
 // see: https://github.com/growilabs/growi/pull/7923
 // see: https://github.com/growilabs/growi/pull/7923
 import { SearchResultList } from './SearchResultList';
 import { SearchResultList } from './SearchResultList';
@@ -26,41 +35,49 @@ import { SearchResultList } from './SearchResultList';
 import styles from './SearchPageBase.module.scss';
 import styles from './SearchPageBase.module.scss';
 
 
 // https://regex101.com/r/brrkBu/1
 // https://regex101.com/r/brrkBu/1
-const highlightKeywordsSplitter = new RegExp('"[^"]+"|[^\u{20}\u{3000}]+', 'ug');
-
+const highlightKeywordsSplitter = /"[^"]+"|[^\u{20}\u{3000}]+/gu;
 
 
 export interface IReturnSelectedPageIds {
 export interface IReturnSelectedPageIds {
-  getSelectedPageIds?: () => Set<string>,
+  getSelectedPageIds?: () => Set<string>;
 }
 }
 
 
-
 type Props = {
 type Props = {
-  className?: string,
-  pages?: IPageWithSearchMeta[],
-  searchingKeyword?: string,
+  className?: string;
+  pages?: IPageWithSearchMeta[];
+  searchingKeyword?: string;
 
 
-  forceHideMenuItems?: ForceHideMenuItems,
+  forceHideMenuItems?: ForceHideMenuItems;
 
 
-  onSelectedPagesByCheckboxesChanged?: (selectedCount: number, totalCount: number) => void,
+  onSelectedPagesByCheckboxesChanged?: (
+    selectedCount: number,
+    totalCount: number,
+  ) => void;
 
 
-  searchControl: React.ReactNode,
-  searchResultListHead: JSX.Element,
-  searchPager: React.ReactNode,
-}
-
-const SearchResultContent = dynamic(() => import('./SearchResultContent').then(mod => mod.SearchResultContent), {
-  ssr: false,
-  loading: () => <></>,
-});
-const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturnSelectedPageIds, Props> = (props: Props, ref) => {
+  searchControl: React.ReactNode;
+  searchResultListHead: JSX.Element;
+  searchPager: React.ReactNode;
+};
 
 
+const SearchResultContent = dynamic(
+  () => import('./SearchResultContent').then((mod) => mod.SearchResultContent),
+  {
+    ssr: false,
+    loading: () => <></>,
+  },
+);
+const SearchPageBaseSubstance: ForwardRefRenderFunction<
+  ISelectableAll & IReturnSelectedPageIds,
+  Props
+> = (props: Props, ref) => {
   const {
   const {
     className,
     className,
     pages,
     pages,
     searchingKeyword,
     searchingKeyword,
     forceHideMenuItems,
     forceHideMenuItems,
     onSelectedPagesByCheckboxesChanged,
     onSelectedPagesByCheckboxesChanged,
-    searchControl, searchResultListHead, searchPager,
+    searchControl,
+    searchResultListHead,
+    searchPager,
   } = props;
   } = props;
 
 
   const searchResultListRef = useRef<ISelectableAll | null>(null);
   const searchResultListRef = useRef<ISelectableAll | null>(null);
@@ -73,7 +90,9 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
   const [selectedPageIdsByCheckboxes] = useState<Set<string>>(new Set());
   const [selectedPageIdsByCheckboxes] = useState<Set<string>>(new Set());
   // const [allPageIds] = useState<Set<string>>(new Set());
   // const [allPageIds] = useState<Set<string>>(new Set());
 
 
-  const [selectedPageWithMeta, setSelectedPageWithMeta] = useState<IPageWithSearchMeta | undefined>();
+  const [selectedPageWithMeta, setSelectedPageWithMeta] = useState<
+    IPageWithSearchMeta | undefined
+  >();
 
 
   // publish selectAll()
   // publish selectAll()
   useImperativeHandle(ref, () => ({
   useImperativeHandle(ref, () => ({
@@ -84,7 +103,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
       }
       }
 
 
       if (pages != null) {
       if (pages != null) {
-        pages.forEach(page => selectedPageIdsByCheckboxes.add(page.data._id));
+        pages.forEach((page) => selectedPageIdsByCheckboxes.add(page.data._id));
       }
       }
     },
     },
     deselectAll: () => {
     deselectAll: () => {
@@ -107,25 +126,26 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
 
 
     if (isChecked) {
     if (isChecked) {
       selectedPageIdsByCheckboxes.add(pageId);
       selectedPageIdsByCheckboxes.add(pageId);
-    }
-    else {
+    } else {
       selectedPageIdsByCheckboxes.delete(pageId);
       selectedPageIdsByCheckboxes.delete(pageId);
     }
     }
 
 
     if (onSelectedPagesByCheckboxesChanged != null) {
     if (onSelectedPagesByCheckboxesChanged != null) {
-      onSelectedPagesByCheckboxesChanged(selectedPageIdsByCheckboxes.size, pages.length);
+      onSelectedPagesByCheckboxesChanged(
+        selectedPageIdsByCheckboxes.size,
+        pages.length,
+      );
     }
     }
   };
   };
 
 
   // select first item on load
   // select first item on load
   useEffect(() => {
   useEffect(() => {
-    if ((pages == null || pages.length === 0)) {
+    if (pages == null || pages.length === 0) {
       setSelectedPageWithMeta(undefined);
       setSelectedPageWithMeta(undefined);
-    }
-    else if ((pages != null && pages.length > 0)) {
+    } else if (pages != null && pages.length > 0) {
       setSelectedPageWithMeta(pages[0]);
       setSelectedPageWithMeta(pages[0]);
     }
     }
-  }, [pages, setSelectedPageWithMeta]);
+  }, [pages]);
 
 
   // reset selectedPageIdsByCheckboxes
   // reset selectedPageIdsByCheckboxes
   useEffect(() => {
   useEffect(() => {
@@ -138,7 +158,10 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
     }
     }
 
 
     if (onSelectedPagesByCheckboxesChanged != null) {
     if (onSelectedPagesByCheckboxesChanged != null) {
-      onSelectedPagesByCheckboxesChanged(selectedPageIdsByCheckboxes.size, pages.length);
+      onSelectedPagesByCheckboxesChanged(
+        selectedPageIdsByCheckboxes.size,
+        pages.length,
+      );
     }
     }
   }, [onSelectedPagesByCheckboxesChanged, pages, selectedPageIdsByCheckboxes]);
   }, [onSelectedPagesByCheckboxesChanged, pages, selectedPageIdsByCheckboxes]);
 
 
@@ -159,28 +182,37 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
       <div className="container-lg grw-container-convertible">
       <div className="container-lg grw-container-convertible">
         <div className="row mt-5">
         <div className="row mt-5">
           <div className="col text-muted">
           <div className="col text-muted">
-            <h1>Search service occures errors. Please contact to administrators of this system.</h1>
+            <h1>
+              Search service occures errors. Please contact to administrators of
+              this system.
+            </h1>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
     );
     );
   }
   }
 
 
-  const highlightKeywords = searchingKeyword != null
-    // Remove double quotation marks before and after a keyword if present
-    // https://regex101.com/r/4QKBwg/1
-    ? searchingKeyword.match(highlightKeywordsSplitter)?.map(keyword => keyword.replace(/^"(.*)"$/, '$1')) ?? undefined
-    : undefined;
+  const highlightKeywords =
+    searchingKeyword != null
+      ? // Remove double quotation marks before and after a keyword if present
+        // https://regex101.com/r/4QKBwg/1
+        (searchingKeyword
+          .match(highlightKeywordsSplitter)
+          ?.map((keyword) => keyword.replace(/^"(.*)"$/, '$1')) ?? undefined)
+      : undefined;
 
 
   return (
   return (
-    <div className={`${className ?? ''} search-result-base flex-grow-1 d-flex flex-expand-vh-100`} data-testid="search-result-base">
-
-      <div className="flex-expand-vert border boder-gray search-result-list" id="search-result-list">
-
+    <div
+      className={`${className ?? ''} search-result-base flex-grow-1 d-flex flex-expand-vh-100`}
+      data-testid="search-result-base"
+    >
+      <div
+        className="flex-expand-vert border boder-gray search-result-list"
+        id="search-result-list"
+      >
         {searchControl}
         {searchControl}
 
 
         <div className="overflow-y-scroll">
         <div className="overflow-y-scroll">
-
           {/* Loading */}
           {/* Loading */}
           {pages == null && (
           {pages == null && (
             <div className="mw-0 flex-grow-1 flex-basis-0 m-5 text-muted text-center">
             <div className="mw-0 flex-grow-1 flex-basis-0 m-5 text-muted text-center">
@@ -191,9 +223,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
           {/* Loaded */}
           {/* Loaded */}
           {pages != null && (
           {pages != null && (
             <>
             <>
-              <div className="my-3 px-md-4 px-3">
-                {searchResultListHead}
-              </div>
+              <div className="my-3 px-md-4 px-3">{searchResultListHead}</div>
 
 
               {pages.length > 0 && (
               {pages.length > 0 && (
                 <div className={`page-list ${styles['page-list']} px-md-4`}>
                 <div className={`page-list ${styles['page-list']} px-md-4`}>
@@ -202,7 +232,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
                     pages={pages}
                     pages={pages}
                     selectedPageId={selectedPageWithMeta?.data._id}
                     selectedPageId={selectedPageWithMeta?.data._id}
                     forceHideMenuItems={forceHideMenuItems}
                     forceHideMenuItems={forceHideMenuItems}
-                    onPageSelected={page => (setSelectedPageWithMeta(page))}
+                    onPageSelected={(page) => setSelectedPageWithMeta(page)}
                     onCheckboxChanged={checkboxChangedHandler}
                     onCheckboxChanged={checkboxChangedHandler}
                   />
                   />
                 </div>
                 </div>
@@ -212,35 +242,34 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
               </div>
               </div>
             </>
             </>
           )}
           )}
-
         </div>
         </div>
-
       </div>
       </div>
 
 
-      <div className={`${styles['search-result-content']} flex-expand-vert d-none d-lg-flex`}>
-        {pages != null && pages.length !== 0 && selectedPageWithMeta != null && (
-          <SearchResultContent
-            pageWithMeta={selectedPageWithMeta}
-            highlightKeywords={highlightKeywords}
-            showPageControlDropdown={!(isGuestUser || isReadOnlyUser)}
-            forceHideMenuItems={forceHideMenuItems}
-          />
-        )}
+      <div
+        className={`${styles['search-result-content']} flex-expand-vert d-none d-lg-flex`}
+      >
+        {pages != null &&
+          pages.length !== 0 &&
+          selectedPageWithMeta != null && (
+            <SearchResultContent
+              pageWithMeta={selectedPageWithMeta}
+              highlightKeywords={highlightKeywords}
+              showPageControlDropdown={!(isGuestUser || isReadOnlyUser)}
+              forceHideMenuItems={forceHideMenuItems}
+            />
+          )}
       </div>
       </div>
-
     </div>
     </div>
   );
   );
 };
 };
 
 
-
 type VoidFunction = () => void;
 type VoidFunction = () => void;
 
 
 export const usePageDeleteModalForBulkDeletion = (
 export const usePageDeleteModalForBulkDeletion = (
-    data: IFormattedSearchResult | undefined,
-    ref: React.MutableRefObject<(ISelectableAll & IReturnSelectedPageIds) | null>,
-    onDeleted?: OnDeletedFunction,
+  data: IFormattedSearchResult | undefined,
+  ref: React.MutableRefObject<(ISelectableAll & IReturnSelectedPageIds) | null>,
+  onDeleted?: OnDeletedFunction,
 ): VoidFunction => {
 ): VoidFunction => {
-
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const { open: openDeleteModal } = usePageDeleteModalActions();
   const { open: openDeleteModal } = usePageDeleteModalActions();
@@ -261,8 +290,9 @@ export const usePageDeleteModalForBulkDeletion = (
       return;
       return;
     }
     }
 
 
-    const selectedPages = data.data
-      .filter(pageWithMeta => selectedPageIds.has(pageWithMeta.data._id));
+    const selectedPages = data.data.filter((pageWithMeta) =>
+      selectedPageIds.has(pageWithMeta.data._id),
+    );
 
 
     openDeleteModal(selectedPages, {
     openDeleteModal(selectedPages, {
       onDeleted: (...args) => {
       onDeleted: (...args) => {
@@ -270,8 +300,7 @@ export const usePageDeleteModalForBulkDeletion = (
         const isCompletely = args[2];
         const isCompletely = args[2];
         if (path == null || isCompletely == null) {
         if (path == null || isCompletely == null) {
           toastSuccess(t('deleted_page'));
           toastSuccess(t('deleted_page'));
-        }
-        else {
+        } else {
           toastSuccess(t('deleted_pages_completely', { path }));
           toastSuccess(t('deleted_pages_completely', { path }));
         }
         }
         mutatePageTree();
         mutatePageTree();
@@ -283,8 +312,6 @@ export const usePageDeleteModalForBulkDeletion = (
       },
       },
     });
     });
   };
   };
-
 };
 };
 
 
-
 export const SearchPageBase = forwardRef(SearchPageBaseSubstance);
 export const SearchPageBase = forwardRef(SearchPageBaseSubstance);

+ 0 - 0
apps/app/src/client/components/SearchPage/SearchResultContent.module.scss → apps/app/src/features/search/client/components/SearchPage/SearchResultContent.module.scss


+ 148 - 83
apps/app/src/client/components/SearchPage/SearchResultContent.tsx → apps/app/src/features/search/client/components/SearchPage/SearchResultContent.tsx

@@ -1,51 +1,74 @@
 import type { FC, JSX } from 'react';
 import type { FC, JSX } from 'react';
-import React, {
-  useCallback, useEffect, useRef,
-} from 'react';
-
-import { getIdStringForRef } from '@growi/core';
+import { useCallback, useEffect, useRef } from 'react';
+import dynamic from 'next/dynamic';
 import type { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '@growi/core';
 import type { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '@growi/core';
+import { getIdStringForRef } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import dynamic from 'next/dynamic';
 import { animateScroll } from 'react-scroll';
 import { animateScroll } from 'react-scroll';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
+import type {
+  AdditionalMenuItemsRendererProps,
+  ForceHideMenuItems,
+} from '~/client/components/Common/Dropdown/PageItemControl';
+import type { RevisionLoaderProps } from '~/client/components/Page/RevisionLoader';
 import { exportAsMarkdown } from '~/client/services/page-operation';
 import { exportAsMarkdown } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/toastr';
 import { toastSuccess } from '~/client/util/toastr';
 import { PagePathNav } from '~/components/Common/PagePathNav';
 import { PagePathNav } from '~/components/Common/PagePathNav';
 import type { IPageWithSearchMeta } from '~/interfaces/search';
 import type { IPageWithSearchMeta } from '~/interfaces/search';
-import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
+import type {
+  OnDeletedFunction,
+  OnDuplicatedFunction,
+  OnRenamedFunction,
+} from '~/interfaces/ui';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { useCurrentUser } from '~/states/global';
 import { useCurrentUser } from '~/states/global';
 import { usePageDeleteModalActions } from '~/states/ui/modal/page-delete';
 import { usePageDeleteModalActions } from '~/states/ui/modal/page-delete';
 import { usePageDuplicateModalActions } from '~/states/ui/modal/page-duplicate';
 import { usePageDuplicateModalActions } from '~/states/ui/modal/page-duplicate';
 import { usePageRenameModalActions } from '~/states/ui/modal/page-rename';
 import { usePageRenameModalActions } from '~/states/ui/modal/page-rename';
-import { mutatePageList, mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
+import {
+  mutatePageList,
+  mutatePageTree,
+  mutateRecentlyUpdated,
+} from '~/stores/page-listing';
 import { useSearchResultOptions } from '~/stores/renderer';
 import { useSearchResultOptions } from '~/stores/renderer';
 import { mutateSearching } from '~/stores/search';
 import { mutateSearching } from '~/stores/search';
 
 
-import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import type { RevisionLoaderProps } from '../Page/RevisionLoader';
-
 import styles from './SearchResultContent.module.scss';
 import styles from './SearchResultContent.module.scss';
 
 
 const moduleClass = styles['search-result-content'];
 const moduleClass = styles['search-result-content'];
 const _fluidLayoutClass = styles['fluid-layout'];
 const _fluidLayoutClass = styles['fluid-layout'];
 
 
-
-const PageControls = dynamic(() => import('../PageControls').then(mod => mod.PageControls), { ssr: false });
-const RevisionLoader = dynamic<RevisionLoaderProps>(() => import('../Page/RevisionLoader').then(mod => mod.RevisionLoader), { ssr: false });
-const PageComment = dynamic(() => import('../PageComment').then(mod => mod.PageComment), { ssr: false });
+const PageControls = dynamic(
+  () =>
+    import('~/client/components/PageControls').then((mod) => mod.PageControls),
+  { ssr: false },
+);
+const RevisionLoader = dynamic<RevisionLoaderProps>(
+  () =>
+    import('~/client/components/Page/RevisionLoader').then(
+      (mod) => mod.RevisionLoader,
+    ),
+  { ssr: false },
+);
+const PageComment = dynamic(
+  () =>
+    import('~/client/components/PageComment').then((mod) => mod.PageComment),
+  { ssr: false },
+);
 const PageContentFooter = dynamic(
 const PageContentFooter = dynamic(
-  () => import('~/components/PageView/PageContentFooter').then(mod => mod.PageContentFooter),
+  () =>
+    import('~/components/PageView/PageContentFooter').then(
+      (mod) => mod.PageContentFooter,
+    ),
   { ssr: false },
   { ssr: false },
 );
 );
 
 
 type AdditionalMenuItemsProps = AdditionalMenuItemsRendererProps & {
 type AdditionalMenuItemsProps = AdditionalMenuItemsRendererProps & {
-  pageId: string,
-  revisionId: string,
-}
+  pageId: string;
+  revisionId: string;
+};
 
 
 const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
 const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -58,7 +81,9 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
       onClick={() => exportAsMarkdown(pageId, revisionId, 'md')}
       onClick={() => exportAsMarkdown(pageId, revisionId, 'md')}
       className="grw-page-control-dropdown-item"
       className="grw-page-control-dropdown-item"
     >
     >
-      <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">cloud_download</span>
+      <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">
+        cloud_download
+      </span>
       {t('page_export.export_page_markdown')}
       {t('page_export.export_page_markdown')}
     </DropdownItem>
     </DropdownItem>
   );
   );
@@ -67,31 +92,38 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
 const SCROLL_OFFSET_TOP = 30;
 const SCROLL_OFFSET_TOP = 30;
 const MUTATION_OBSERVER_CONFIG = { childList: true, subtree: true }; // omit 'subtree: true'
 const MUTATION_OBSERVER_CONFIG = { childList: true, subtree: true }; // omit 'subtree: true'
 
 
-type Props ={
-  pageWithMeta : IPageWithSearchMeta,
-  highlightKeywords?: string[],
-  showPageControlDropdown?: boolean,
-  forceHideMenuItems?: ForceHideMenuItems,
-}
+type Props = {
+  pageWithMeta: IPageWithSearchMeta;
+  highlightKeywords?: string[];
+  showPageControlDropdown?: boolean;
+  forceHideMenuItems?: ForceHideMenuItems;
+};
 
 
 const scrollToFirstHighlightedKeyword = (scrollElement: HTMLElement): void => {
 const scrollToFirstHighlightedKeyword = (scrollElement: HTMLElement): void => {
   // use querySelector to intentionally get the first element found
   // use querySelector to intentionally get the first element found
-  const toElem = scrollElement.querySelector('.highlighted-keyword') as HTMLElement | null;
+  const toElem = scrollElement.querySelector(
+    '.highlighted-keyword',
+  ) as HTMLElement | null;
   if (toElem == null) {
   if (toElem == null) {
     return;
     return;
   }
   }
 
 
-  const distance = toElem.getBoundingClientRect().top - scrollElement.getBoundingClientRect().top - SCROLL_OFFSET_TOP;
+  const distance =
+    toElem.getBoundingClientRect().top -
+    scrollElement.getBoundingClientRect().top -
+    SCROLL_OFFSET_TOP;
   animateScroll.scrollMore(distance, {
   animateScroll.scrollMore(distance, {
     containerId: scrollElement.id,
     containerId: scrollElement.id,
     duration: 200,
     duration: 200,
   });
   });
 };
 };
-const scrollToFirstHighlightedKeywordDebounced = debounce(500, scrollToFirstHighlightedKeyword);
+const scrollToFirstHighlightedKeywordDebounced = debounce(
+  500,
+  scrollToFirstHighlightedKeyword,
+);
 
 
 export const SearchResultContent: FC<Props> = (props: Props) => {
 export const SearchResultContent: FC<Props> = (props: Props) => {
-
-  const scrollElementRef = useRef<HTMLDivElement|null>(null);
+  const scrollElementRef = useRef<HTMLDivElement | null>(null);
 
 
   // ***************************  Auto Scroll  ***************************
   // ***************************  Auto Scroll  ***************************
   useEffect(() => {
   useEffect(() => {
@@ -124,67 +156,89 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   const { open: openDuplicateModal } = usePageDuplicateModalActions();
   const { open: openDuplicateModal } = usePageDuplicateModalActions();
   const { open: openRenameModal } = usePageRenameModalActions();
   const { open: openRenameModal } = usePageRenameModalActions();
   const { open: openDeleteModal } = usePageDeleteModalActions();
   const { open: openDeleteModal } = usePageDeleteModalActions();
-  const { data: rendererOptions } = useSearchResultOptions(pageWithMeta.data.path, highlightKeywords);
+  const { data: rendererOptions } = useSearchResultOptions(
+    pageWithMeta.data.path,
+    highlightKeywords,
+  );
   const currentUser = useCurrentUser();
   const currentUser = useCurrentUser();
 
 
   const shouldExpandContent = useShouldExpandContent(page);
   const shouldExpandContent = useShouldExpandContent(page);
 
 
-  const duplicateItemClickedHandler = useCallback(async(pageToDuplicate) => {
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
-      toastSuccess(t('duplicated_pages', { fromPath }));
-
-      mutatePageTree();
-      mutateRecentlyUpdated();
-      mutateSearching();
-      mutatePageList();
-    };
-    openDuplicateModal(pageToDuplicate, { onDuplicated: duplicatedHandler });
-  }, [openDuplicateModal, t]);
+  const duplicateItemClickedHandler = useCallback(
+    async (pageToDuplicate) => {
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
+        toastSuccess(t('duplicated_pages', { fromPath }));
+
+        mutatePageTree();
+        mutateRecentlyUpdated();
+        mutateSearching();
+        mutatePageList();
+      };
+      openDuplicateModal(pageToDuplicate, { onDuplicated: duplicatedHandler });
+    },
+    [openDuplicateModal, t],
+  );
 
 
-  const renameItemClickedHandler = useCallback((pageToRename: IPageToRenameWithMeta) => {
-    const renamedHandler: OnRenamedFunction = (path) => {
-      toastSuccess(t('renamed_pages', { path }));
+  const renameItemClickedHandler = useCallback(
+    (pageToRename: IPageToRenameWithMeta) => {
+      const renamedHandler: OnRenamedFunction = (path) => {
+        toastSuccess(t('renamed_pages', { path }));
+
+        mutatePageTree();
+        mutateRecentlyUpdated();
+        mutateSearching();
+        mutatePageList();
+      };
+      openRenameModal(pageToRename, { onRenamed: renamedHandler });
+    },
+    [openRenameModal, t],
+  );
 
 
+  const onDeletedHandler: OnDeletedFunction = useCallback(
+    (pathOrPathsToDelete, isRecursively, isCompletely) => {
+      if (typeof pathOrPathsToDelete !== 'string') {
+        return;
+      }
+      const path = pathOrPathsToDelete;
+
+      if (isCompletely) {
+        toastSuccess(t('deleted_pages_completely', { path }));
+      } else {
+        toastSuccess(t('deleted_pages', { path }));
+      }
       mutatePageTree();
       mutatePageTree();
       mutateRecentlyUpdated();
       mutateRecentlyUpdated();
       mutateSearching();
       mutateSearching();
       mutatePageList();
       mutatePageList();
-    };
-    openRenameModal(pageToRename, { onRenamed: renamedHandler });
-  }, [openRenameModal, t]);
-
-  const onDeletedHandler: OnDeletedFunction = useCallback((pathOrPathsToDelete, isRecursively, isCompletely) => {
-    if (typeof pathOrPathsToDelete !== 'string') {
-      return;
-    }
-    const path = pathOrPathsToDelete;
-
-    if (isCompletely) {
-      toastSuccess(t('deleted_pages_completely', { path }));
-    }
-    else {
-      toastSuccess(t('deleted_pages', { path }));
-    }
-    mutatePageTree();
-    mutateRecentlyUpdated();
-    mutateSearching();
-    mutatePageList();
-  }, [t]);
+    },
+    [t],
+  );
 
 
-  const deleteItemClickedHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
-    openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
-  }, [onDeletedHandler, openDeleteModal]);
+  const deleteItemClickedHandler = useCallback(
+    (pageToDelete: IPageToDeleteWithMeta) => {
+      openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
+    },
+    [onDeletedHandler, openDeleteModal],
+  );
 
 
   const RightComponent = useCallback(() => {
   const RightComponent = useCallback(() => {
     if (page == null) {
     if (page == null) {
       return <></>;
       return <></>;
     }
     }
 
 
-    const revisionId = page.revision != null ? getIdStringForRef(page.revision) : null;
-    const additionalMenuItemRenderer = revisionId != null
-      ? props => <AdditionalMenuItems {...props} pageId={page._id} revisionId={revisionId} />
-      : undefined;
+    const revisionId =
+      page.revision != null ? getIdStringForRef(page.revision) : null;
+    const additionalMenuItemRenderer =
+      revisionId != null
+        ? (props) => (
+            <AdditionalMenuItems
+              {...props}
+              pageId={page._id}
+              revisionId={revisionId}
+            />
+          )
+        : undefined;
 
 
     return (
     return (
       <div className="d-flex flex-column flex-row-reverse flex px-2 py-1">
       <div className="d-flex flex-column flex-row-reverse flex px-2 py-1">
@@ -202,8 +256,15 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
         />
         />
       </div>
       </div>
     );
     );
-  }, [page, shouldExpandContent, showPageControlDropdown, forceHideMenuItems,
-      duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler]);
+  }, [
+    page,
+    shouldExpandContent,
+    showPageControlDropdown,
+    forceHideMenuItems,
+    duplicateItemClickedHandler,
+    renameItemClickedHandler,
+    deleteItemClickedHandler,
+  ]);
 
 
   const fluidLayoutClass = shouldExpandContent ? _fluidLayoutClass : '';
   const fluidLayoutClass = shouldExpandContent ? _fluidLayoutClass : '';
 
 
@@ -216,7 +277,13 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       <RightComponent />
       <RightComponent />
 
 
       <div className="container-lg grw-container-convertible pt-2 pb-2">
       <div className="container-lg grw-container-convertible pt-2 pb-2">
-        <PagePathNav pageId={page._id} pagePath={page.path} isWipPage={page.wip} formerLinkClassName="small" latterLinkClassName="fs-3 text-truncate" />
+        <PagePathNav
+          pageId={page._id}
+          pagePath={page.path}
+          isWipPage={page.wip}
+          formerLinkClassName="small"
+          latterLinkClassName="fs-3 text-truncate"
+        />
       </div>
       </div>
 
 
       <div
       <div
@@ -224,14 +291,14 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
         ref={scrollElementRef}
         ref={scrollElementRef}
         className="search-result-content-body-container container-lg grw-container-convertible overflow-y-scroll"
         className="search-result-content-body-container container-lg grw-container-convertible overflow-y-scroll"
       >
       >
-        { page.revision != null && rendererOptions != null && (
+        {page.revision != null && rendererOptions != null && (
           <RevisionLoader
           <RevisionLoader
             rendererOptions={rendererOptions}
             rendererOptions={rendererOptions}
             pageId={page._id}
             pageId={page._id}
             revisionId={page.revision}
             revisionId={page.revision}
           />
           />
         )}
         )}
-        { page.revision != null && (
+        {page.revision != null && (
           <PageComment
           <PageComment
             rendererOptions={rendererOptions}
             rendererOptions={rendererOptions}
             pageId={page._id}
             pageId={page._id}
@@ -242,9 +309,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           />
           />
         )}
         )}
 
 
-        <PageContentFooter
-          page={page}
-        />
+        <PageContentFooter page={page} />
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 15 - 10
apps/app/src/client/components/SearchPage/SearchResultList.tsx → apps/app/src/features/search/client/components/SearchPage/SearchResultList.tsx

@@ -1,23 +1,28 @@
 import type { ForwardRefRenderFunction } from 'react';
 import type { ForwardRefRenderFunction } from 'react';
-import React, {
-  forwardRef, useCallback, useImperativeHandle, useRef,
-} from 'react';
-
+import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
 import {
 import {
-  type IPageInfoForListing, type IPageWithMeta, isIPageInfoForListing,
+  type IPageInfoForListing,
+  type IPageWithMeta,
+  isIPageInfoForListing,
 } from '@growi/core/dist/interfaces';
 } from '@growi/core/dist/interfaces';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import type { ISelectable, ISelectableAll } from '~/client/interfaces/selectable-all';
+import type { ForceHideMenuItems } from '~/client/components/Common/Dropdown/PageItemControl';
+import { PageListItemL } from '~/client/components/PageList/PageListItemL';
+import type {
+  ISelectable,
+  ISelectableAll,
+} from '~/client/interfaces/selectable-all';
 import { toastSuccess } from '~/client/util/toastr';
 import { toastSuccess } from '~/client/util/toastr';
 import type { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
 import type { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
-import { mutatePageTree, useSWRxPageInfoForList, mutateRecentlyUpdated } from '~/stores/page-listing';
+import {
+  mutatePageTree,
+  mutateRecentlyUpdated,
+  useSWRxPageInfoForList,
+} from '~/stores/page-listing';
 import { mutateSearching } from '~/stores/search';
 import { mutateSearching } from '~/stores/search';
 
 
-import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import { PageListItemL } from '../PageList/PageListItemL';
-
 type Props = {
 type Props = {
   pages: IPageWithSearchMeta[],
   pages: IPageWithSearchMeta[],
   selectedPageId?: string,
   selectedPageId?: string,

+ 0 - 0
apps/app/src/client/components/SearchPage/SortControl.module.scss → apps/app/src/features/search/client/components/SearchPage/SortControl.module.scss


+ 17 - 15
apps/app/src/client/components/SearchPage/SortControl.tsx → apps/app/src/features/search/client/components/SearchPage/SortControl.tsx

@@ -1,33 +1,32 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
-import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { SORT_AXIS, SORT_ORDER } from '../../../interfaces/search';
+import { SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 
 
 import styles from './SortControl.module.scss';
 import styles from './SortControl.module.scss';
 
 
 const { DESC, ASC } = SORT_ORDER;
 const { DESC, ASC } = SORT_ORDER;
 
 
 type Props = {
 type Props = {
-  sort: SORT_AXIS,
-  order: SORT_ORDER,
-  onChange?: (nextSort: SORT_AXIS, nextOrder: SORT_ORDER) => void,
-}
-
-const SortControl: FC <Props> = (props: Props) => {
+  sort: SORT_AXIS;
+  order: SORT_ORDER;
+  onChange?: (nextSort: SORT_AXIS, nextOrder: SORT_ORDER) => void;
+};
 
 
+const SortControl: FC<Props> = (props: Props) => {
   const { t } = useTranslation('');
   const { t } = useTranslation('');
 
 
   const { sort, order, onChange } = props;
   const { sort, order, onChange } = props;
 
 
-  const onClickChangeSort = (nextSortAxis: SORT_AXIS, nextSortOrder: SORT_ORDER) => {
+  const onClickChangeSort = (
+    nextSortAxis: SORT_AXIS,
+    nextSortOrder: SORT_ORDER,
+  ) => {
     if (onChange != null) {
     if (onChange != null) {
       onChange(nextSortAxis, nextSortOrder);
       onChange(nextSortAxis, nextSortOrder);
     }
     }
   };
   };
 
 
-
   return (
   return (
     <>
     <>
       <div className={`btn-group ${styles['sort-control']}`}>
       <div className={`btn-group ${styles['sort-control']}`}>
@@ -38,18 +37,22 @@ const SortControl: FC <Props> = (props: Props) => {
           aria-expanded="false"
           aria-expanded="false"
         >
         >
           <span className="material-symbols-outlined py-0">sort</span>
           <span className="material-symbols-outlined py-0">sort</span>
-          <span className="ms-2 me-auto">{t(`search_result.sort_axis.${sort}`)}</span>
+          <span className="ms-2 me-auto">
+            {t(`search_result.sort_axis.${sort}`)}
+          </span>
           <span className="material-symbols-outlined py-0">expand_more</span>
           <span className="material-symbols-outlined py-0">expand_more</span>
         </button>
         </button>
         <ul className="dropdown-menu">
         <ul className="dropdown-menu">
           {Object.values(SORT_AXIS).map((sortAxis) => {
           {Object.values(SORT_AXIS).map((sortAxis) => {
-            const nextOrder = (sort !== sortAxis || order === ASC) ? DESC : ASC;
+            const nextOrder = sort !== sortAxis || order === ASC ? DESC : ASC;
             return (
             return (
               <button
               <button
                 key={sortAxis}
                 key={sortAxis}
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
-                onClick={() => { onClickChangeSort(sortAxis, nextOrder) }}
+                onClick={() => {
+                  onClickChangeSort(sortAxis, nextOrder);
+                }}
               >
               >
                 <span>{t(`search_result.sort_axis.${sortAxis}`)}</span>
                 <span>{t(`search_result.sort_axis.${sortAxis}`)}</span>
               </button>
               </button>
@@ -61,5 +64,4 @@ const SortControl: FC <Props> = (props: Props) => {
   );
   );
 };
 };
 
 
-
 export default SortControl;
 export default SortControl;

+ 1 - 0
apps/app/src/features/search/client/components/SearchPage/index.ts

@@ -0,0 +1 @@
+export * from './SearchPage';

+ 47 - 0
apps/app/src/features/search/client/states/modal/search.ts

@@ -0,0 +1,47 @@
+import { useCallback } from 'react';
+import { atom, useAtomValue, useSetAtom } from 'jotai';
+
+export type SearchModalStatus = {
+  isOpened: boolean;
+  searchKeyword?: string;
+};
+
+export type SearchModalActions = {
+  open: (keywordOnInit?: string) => void;
+  close: () => void;
+};
+
+// Atom definition
+const searchModalAtom = atom<SearchModalStatus>({
+  isOpened: false,
+  searchKeyword: undefined,
+});
+
+// Read-only hook (useAtomValue)
+export const useSearchModalStatus = (): SearchModalStatus => {
+  return useAtomValue(searchModalAtom);
+};
+
+// Actions hook (useSetAtom + useCallback)
+export const useSearchModalActions = (): SearchModalActions => {
+  const setStatus = useSetAtom(searchModalAtom);
+
+  const open = useCallback(
+    (keywordOnInit?: string) => {
+      setStatus({
+        isOpened: true,
+        searchKeyword: keywordOnInit,
+      });
+    },
+    [setStatus],
+  );
+
+  const close = useCallback(() => {
+    setStatus({
+      isOpened: false,
+      searchKeyword: undefined,
+    });
+  }, [setStatus]);
+
+  return { open, close };
+};

+ 0 - 39
apps/app/src/features/search/client/stores/search.ts

@@ -1,39 +0,0 @@
-import { useCallback } from 'react';
-
-import type { SWRResponse } from 'swr';
-
-import { useStaticSWR } from '~/stores/use-static-swr';
-
-type SearchModalStatus = {
-  isOpened: boolean;
-  searchKeyword?: string;
-};
-
-type SearchModalUtils = {
-  open(keywordOnInit?: string): void;
-  close(): void;
-};
-export const useSearchModal = (
-  status?: SearchModalStatus,
-): SWRResponse<SearchModalStatus, Error> & SearchModalUtils => {
-  const initialStatus = { isOpened: false };
-  const swrResponse = useStaticSWR<SearchModalStatus, Error>(
-    'SearchModal',
-    status,
-    { fallbackData: initialStatus },
-  );
-
-  return {
-    ...swrResponse,
-    open: useCallback(
-      (keywordOnInit?: string) => {
-        swrResponse.mutate({ isOpened: true, searchKeyword: keywordOnInit });
-      },
-      [swrResponse],
-    ),
-    close: useCallback(
-      () => swrResponse.mutate({ isOpened: false }),
-      [swrResponse],
-    ),
-  };
-};

+ 1 - 1
apps/app/src/pages/_private-legacy-pages/index.page.tsx

@@ -28,7 +28,7 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
   const router = useRouter();
   const router = useRouter();
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const PrivateLegacyPages = dynamic(() => import('~/client/components/PrivateLegacyPages'), { ssr: false });
+  const PrivateLegacyPages = dynamic(() => import('~/features/search/client/components/PrivateLegacyPages'), { ssr: false });
 
 
   // clear the cache for the current page
   // clear the cache for the current page
   //  in order to fix https://redmine.weseek.co.jp/issues/135811
   //  in order to fix https://redmine.weseek.co.jp/issues/135811

+ 1 - 1
apps/app/src/pages/_search/index.page.tsx

@@ -21,7 +21,7 @@ import { useHydrateServerConfigurationAtoms } from './use-hydrate-server-configu
 
 
 
 
 const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
 const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
-const SearchPage = dynamic(() => import('~/client/components/SearchPage').then(mod => mod.SearchPage), { ssr: false });
+const SearchPage = dynamic(() => import('~/features/search/client/components/SearchPage').then(mod => mod.SearchPage), { ssr: false });
 
 
 type Props = CommonInitialProps & CommonEachProps & BasicLayoutConfigurationProps & ServerConfigurationProps & RendererConfigProps;
 type Props = CommonInitialProps & CommonEachProps & BasicLayoutConfigurationProps & ServerConfigurationProps & RendererConfigProps;