Shun Miyazawa 2 lat temu
rodzic
commit
323a545653

+ 10 - 7
apps/app/src/features/search/client/components/SearchForm.tsx

@@ -6,10 +6,11 @@ type Props = {
   searchKeyword: string,
   searchKeyword: string,
   onChangeSearchText?: (text: string) => void,
   onChangeSearchText?: (text: string) => void,
   onClickClearButton?: () => void,
   onClickClearButton?: () => void,
+  getInputProps: any
 }
 }
 export const SearchForm = (props: Props): JSX.Element => {
 export const SearchForm = (props: Props): JSX.Element => {
   const {
   const {
-    searchKeyword, onChangeSearchText, onClickClearButton,
+    searchKeyword, onChangeSearchText, onClickClearButton, getInputProps,
   } = props;
   } = props;
 
 
   const inputRef = useRef<HTMLInputElement>(null);
   const inputRef = useRef<HTMLInputElement>(null);
@@ -33,16 +34,18 @@ export const SearchForm = (props: Props): JSX.Element => {
   });
   });
 
 
   return (
   return (
-    <div className="text-muted d-flex justify-content-center align-items-center ps-1">
+    <div className="text-muted d-flex justify-content-center align-items-center">
       <span className="material-symbols-outlined fs-4 me-3">search</span>
       <span className="material-symbols-outlined fs-4 me-3">search</span>
 
 
       <input
       <input
-        ref={inputRef}
-        type="text"
+        {...getInputProps({
+          ref: inputRef,
+          type: 'text',
+          placeholder: 'Search...',
+          value: searchKeyword,
+        })}
         className="form-control"
         className="form-control"
-        placeholder="Search..."
-        value={searchKeyword}
-        onChange={(e) => { changeSearchTextHandler(e) }}
+        onChange={changeSearchTextHandler}
       />
       />
 
 
       <button
       <button

+ 29 - 0
apps/app/src/features/search/client/components/SearchMenuItem.tsx

@@ -0,0 +1,29 @@
+
+import React from 'react';
+
+type Props = {
+  url: string
+  index: number
+  highlightedIndex: number | null
+  getItemProps: any
+  children: React.ReactNode
+}
+
+export const SearchMenuItem = (props: Props): JSX.Element => {
+  const {
+    url, getItemProps, index, highlightedIndex, children,
+  } = props;
+
+  const option = {
+    index,
+    item: { url },
+    className: 'mb-2 d-flex',
+    style: { backgroundColor: highlightedIndex === index ? 'lightgray' : 'white', pointer: 'cursor' },
+  };
+
+  return (
+    <li className="text-muted d-flex" {...getItemProps(option)}>
+      { children }
+    </li>
+  );
+};

+ 73 - 48
apps/app/src/features/search/client/components/SearchMethodMenuItem.tsx

@@ -4,65 +4,90 @@ import { useTranslation } from 'next-i18next';
 
 
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
 
 
-type MenuItemProps = {
-  children: React.ReactNode
-  onClick: () => void
+import { SearchMenuItem } from './SearchMenuItem';
+
+type ComponentType = 'nomal' | 'tree' | 'exact';
+
+type SearchMethodMenuItemSubstanceProps = {
+  componentType: ComponentType
+  index: number
+  highlightedIndex: number | null
+  getItemProps: any
+  searchKeyword: string
 }
 }
-const MenuItem = (props: MenuItemProps): JSX.Element => {
-  const { children, onClick } = props;
 
 
-  return (
-    <tr>
-      <div className="text-muted ps-1 d-flex">
+const SearchMethodMenuItemSubstance = (props: SearchMethodMenuItemSubstanceProps): JSX.Element => {
+  const {
+    componentType, getItemProps, index, highlightedIndex, searchKeyword,
+  } = props;
+  const { t } = useTranslation('commons');
+  const { data: currentPagePath } = useCurrentPagePath();
+
+  if (componentType === 'nomal') {
+    return (
+      <SearchMenuItem index={index} highlightedIndex={highlightedIndex} getItemProps={getItemProps} url={`/_search?q=${searchKeyword}`}>
         <span className="material-symbols-outlined fs-4 me-3">search</span>
         <span className="material-symbols-outlined fs-4 me-3">search</span>
-        { children }
-      </div>
-    </tr>
-  );
+        <span>{searchKeyword}</span>
+        <div className="ms-auto">
+          <span>{t('search_method_menu_item.search_in_all')}</span>
+        </div>
+      </SearchMenuItem>
+    );
+  }
+
+  if (componentType === 'tree') {
+    return (
+      <SearchMenuItem
+        index={index}
+        highlightedIndex={highlightedIndex}
+        getItemProps={getItemProps}
+        url={`/_search?q=prefix:${currentPagePath} ${searchKeyword}`}
+      >
+        <span className="material-symbols-outlined fs-4 me-3">search</span>
+        <code>prefix: {currentPagePath}</code>
+        <span className="ms-2">{searchKeyword}</span>
+        <div className="ms-auto">
+          <span>{t('search_method_menu_item.only_children_of_this_tree')}</span>
+        </div>
+      </SearchMenuItem>
+    );
+  }
+
+  if (componentType === 'exact') {
+    return (
+      <SearchMenuItem index={index} highlightedIndex={highlightedIndex} getItemProps={getItemProps} url={`/_search?q="${searchKeyword}"`}>
+        <span className="material-symbols-outlined fs-4 me-3">search</span>
+        <span>{`"${searchKeyword}"`}</span>
+        <div className="ms-auto">
+          <span>{t('search_method_menu_item.exact_mutch')}</span>
+        </div>
+      </SearchMenuItem>
+    );
+  }
+
+  return (<></>);
 };
 };
 
 
 
 
-type SearchMethodMenuItemProps = {
-  searchKeyword: string
-}
-export const SearchMethodMenuItem = (props: SearchMethodMenuItemProps): JSX.Element => {
-  const { t } = useTranslation('commons');
+type SearchMethodMenuItemProps = Omit<SearchMethodMenuItemSubstanceProps, 'componentType' | 'index'> & { searchKeyword: string }
 
 
-  const { data: currentPagePath } = useCurrentPagePath();
+export const SearchMethodMenuItem = (props: SearchMethodMenuItemProps): JSX.Element => {
 
 
   const { searchKeyword } = props;
   const { searchKeyword } = props;
 
 
-  const shouldShowButton = searchKeyword.length > 0;
+  const isEmptyKeyword = searchKeyword.trim().length === 0;
+
+  const searchMethodMenuItemData: Array<{ componentType: ComponentType }> = isEmptyKeyword
+    ? [{ componentType: 'tree' }]
+    : [{ componentType: 'nomal' }, { componentType: 'tree' }, { componentType: 'exact' }];
 
 
   return (
   return (
-    <table className="table">
-      <tbody>
-        { shouldShowButton && (
-          <MenuItem onClick={() => {}}>
-            <span>{searchKeyword}</span>
-            <div className="ms-auto">
-              <span>{t('search_method_menu_item.search_in_all')}</span>
-            </div>
-          </MenuItem>
-        )}
-
-        <MenuItem onClick={() => {}}>
-          <code>prefix: {currentPagePath}</code>
-          <span className="ms-2">{searchKeyword}</span>
-          <div className="ms-auto">
-            <span>{t('search_method_menu_item.only_children_of_this_tree')}</span>
-          </div>
-        </MenuItem>
-
-        { shouldShowButton && (
-          <MenuItem onClick={() => {}}>
-            <span>{`"${searchKeyword}"`}</span>
-            <div className="ms-auto">
-              <span>{t('search_method_menu_item.exact_mutch')}</span>
-            </div>
-          </MenuItem>
-        ) }
-      </tbody>
-    </table>
+    <>
+      { searchMethodMenuItemData
+        .map((item, index) => (
+          <SearchMethodMenuItemSubstance key={item.componentType} index={index} componentType={item.componentType} {...props} />
+        ))
+      }
+    </>
   );
   );
 };
 };

+ 22 - 158
apps/app/src/features/search/client/components/SearchModal.tsx

@@ -2,85 +2,16 @@ import React, {
   useState, useCallback, useEffect,
   useState, useCallback, useEffect,
 } from 'react';
 } from 'react';
 
 
-import { PagePathLabel, UserPicture } from '@growi/ui/dist/components';
 import Downshift from 'downshift';
 import Downshift from 'downshift';
-import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
 import { Modal, ModalBody } from 'reactstrap';
 import { Modal, ModalBody } from 'reactstrap';
-import { useDebounce } from 'usehooks-ts';
-
-import { useCurrentPagePath } from '~/stores/page';
-import { useSWRxSearch } from '~/stores/search';
 
 
 import { useSearchModal } from '../stores/search';
 import { useSearchModal } from '../stores/search';
 
 
-
 import { SearchForm } from './SearchForm';
 import { SearchForm } from './SearchForm';
 import { SearchHelp } from './SearchHelp';
 import { SearchHelp } from './SearchHelp';
-
-type ComponentType = 'nomal' | 'tree' | 'exact';
-
-type SearchMenuItemProps = {
-  componentType: ComponentType
-  index: number
-  highlightedIndex: number | null
-  getItemProps: any
-  searchKeyword: string
-}
-
-const SearchMethodMenuItem = (props: SearchMenuItemProps): JSX.Element => {
-  const {
-    componentType, getItemProps, index, highlightedIndex, searchKeyword,
-  } = props;
-  const { t } = useTranslation('commons');
-  const { data: currentPagePath } = useCurrentPagePath();
-
-  const option = {
-    key: componentType,
-    index,
-    item: {},
-    className: 'mb-2 d-flex',
-    style: { backgroundColor: highlightedIndex === index ? 'lightgray' : 'white', pointer: 'cursor' },
-  };
-
-  if (componentType === 'nomal') {
-    return (
-      <li key={componentType} className="text-muted d-flex" {...getItemProps(option)}>
-        <span className="material-symbols-outlined fs-4 me-3">search</span>
-        <span>{searchKeyword}</span>
-        <div className="ms-auto">
-          <span>{t('search_method_menu_item.search_in_all')}</span>
-        </div>
-      </li>
-    );
-  }
-
-  if (componentType === 'tree') {
-    return (
-      <li key={componentType} className="text-muted d-flex" {...getItemProps(option)}>
-        <span className="material-symbols-outlined fs-4 me-3">search</span>
-        <code>prefix: {currentPagePath}</code>
-        <span className="ms-2">{searchKeyword}</span>
-        <div className="ms-auto">
-          <span>{t('search_method_menu_item.only_children_of_this_tree')}</span>
-        </div>
-      </li>
-    );
-  }
-
-  if (componentType === 'exact') {
-    return (
-      <li key={componentType} className="text-muted d-flex" {...getItemProps(option)}>
-        <span className="material-symbols-outlined fs-4 me-3">search</span>
-        <span>{`"${searchKeyword}"`}</span>
-        <div className="ms-auto">
-          <span>{t('search_method_menu_item.exact_mutch')}</span>
-        </div>
-      </li>
-    );
-  }
-
-  return (<></>);
-};
+import { SearchMethodMenuItem } from './SearchMethodMenuItem';
+import { SearchResultMenuItem } from './SearchResultMenuItem';
 
 
 
 
 const SearchModal = (): JSX.Element => {
 const SearchModal = (): JSX.Element => {
@@ -88,6 +19,8 @@ const SearchModal = (): JSX.Element => {
 
 
   const { data: searchModalData, close: closeSearchModal } = useSearchModal();
   const { data: searchModalData, close: closeSearchModal } = useSearchModal();
 
 
+  const router = useRouter();
+
   const changeSearchTextHandler = useCallback((searchText: string) => {
   const changeSearchTextHandler = useCallback((searchText: string) => {
     setSearchKeyword(searchText);
     setSearchKeyword(searchText);
   }, []);
   }, []);
@@ -96,22 +29,10 @@ const SearchModal = (): JSX.Element => {
     setSearchKeyword('');
     setSearchKeyword('');
   }, []);
   }, []);
 
 
-
-  const debouncedKeyword = useDebounce(searchKeyword, 500);
-
-  // const isEmptyKeyword = debouncedKeyword.trim() === '';
-  const isSearchable = searchKeyword.length !== 0;
-
-  const { data: searchResult, isLoading } = useSWRxSearch(isSearchable ? searchKeyword : null, null, { limit: 10 });
-
-  const searchMethodMenuItemData: Array<{ componentType: ComponentType }> = isSearchable
-    ? [{ componentType: 'nomal' }, { componentType: 'tree' }, { componentType: 'exact' }]
-    : [{ componentType: 'tree' }];
-
-
-  const getFiexdIndex = (index: number) => {
-    return index + searchMethodMenuItemData.length;
-  };
+  const selectSearchMenuItemHandler = useCallback((url: string) => {
+    router.push(url);
+    closeSearchModal();
+  }, [closeSearchModal, router]);
 
 
   useEffect(() => {
   useEffect(() => {
     if (!searchModalData?.isOpened) {
     if (!searchModalData?.isOpened) {
@@ -123,7 +44,7 @@ const SearchModal = (): JSX.Element => {
   return (
   return (
     <Modal size="lg" isOpen={searchModalData?.isOpened ?? false} toggle={closeSearchModal}>
     <Modal size="lg" isOpen={searchModalData?.isOpened ?? false} toggle={closeSearchModal}>
       <ModalBody>
       <ModalBody>
-        <Downshift>
+        <Downshift itemToString={undefined} onChange={(selectedItem: { url: string }) => { selectSearchMenuItemHandler(selectedItem.url) }}>
           {({
           {({
             getInputProps,
             getInputProps,
             getItemProps,
             getItemProps,
@@ -131,76 +52,19 @@ const SearchModal = (): JSX.Element => {
             highlightedIndex,
             highlightedIndex,
           }) => (
           }) => (
             <div>
             <div>
-
-              {/* SearchForm */}
-              <div className="text-muted d-flex justify-content-center align-items-center">
-                <span className="material-symbols-outlined fs-4 me-3">search</span>
-
-                <input
-                  {...getInputProps({
-                    type: 'search',
-                    className: 'form-control',
-                    placeholder: 'Search...',
-                    onChange(e: React.ChangeEvent<HTMLInputElement>) {
-                      changeSearchTextHandler(e.target.value);
-                    },
-                  })}
-                />
-
-                <button
-                  type="button"
-                  className="btn border-0 d-flex justify-content-center p-0"
-                  onClick={clickClearButtonHandler}
-                >
-                  <span className="material-symbols-outlined fs-4 ms-3">close</span>
-                </button>
-              </div>
-
-
-              {/* SearchMethodMenuItem */}
-              <ul {...getMenuProps()} className="list-unstyled mt-3">
-                { searchMethodMenuItemData.map((item, index) => (
-                  <SearchMethodMenuItem
-                    componentType={item.componentType}
-                    getItemProps={getItemProps}
-                    index={index}
-                    highlightedIndex={highlightedIndex}
-                    searchKeyword={debouncedKeyword}
-                  />
-                ))}
+              <SearchForm
+                searchKeyword={searchKeyword}
+                onChangeSearchText={changeSearchTextHandler}
+                onClickClearButton={clickClearButtonHandler}
+                getInputProps={getInputProps}
+              />
+
+              <ul {...getMenuProps()} className="list-unstyled">
+                <div className="border-top mt-3 mb-3" />
+                <SearchMethodMenuItem searchKeyword={searchKeyword} getItemProps={getItemProps} highlightedIndex={highlightedIndex} />
+                <div className="border-top mt-3 mb-3" />
+                <SearchResultMenuItem searchKeyword={searchKeyword} getItemProps={getItemProps} highlightedIndex={highlightedIndex} />
               </ul>
               </ul>
-
-
-              {/* SearchResultMenuItem */}
-              { isSearchable && (
-                <ul {...getMenuProps()} className="list-unstyled mt-3">
-                  {searchResult?.data
-                    .map((item, index) => (
-                      <li
-                        {...getItemProps({
-                          key: item.data._id,
-                          index: getFiexdIndex(index),
-                          item: {},
-                          className: 'mb-2 d-flex',
-                          style: { backgroundColor: highlightedIndex === getFiexdIndex(index) ? 'lightgray' : 'white', pointer: 'cursor' },
-                        })}
-                      >
-                        <UserPicture user={item.data.creator} />
-
-                        <span className="ms-3 text-break text-wrap">
-                          <PagePathLabel path={item.data.path} />
-                        </span>
-
-                        <span className="ms-2 text-muted d-flex justify-content-center align-items-center">
-                          <span className="material-symbols-outlined fs-5">footprint</span>
-                          <span>{item.data.seenUsers.length}</span>
-                        </span>
-                      </li>
-                    ))
-                  }
-                </ul>
-
-              )}
             </div>
             </div>
           )}
           )}
         </Downshift>
         </Downshift>

+ 33 - 26
apps/app/src/features/search/client/components/SearchResultMenuItem.tsx

@@ -1,28 +1,39 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
 
 import { PagePathLabel, UserPicture } from '@growi/ui/dist/components';
 import { PagePathLabel, UserPicture } from '@growi/ui/dist/components';
 import { useDebounce } from 'usehooks-ts';
 import { useDebounce } from 'usehooks-ts';
 
 
 import { useSWRxSearch } from '~/stores/search';
 import { useSWRxSearch } from '~/stores/search';
 
 
+import { SearchMenuItem } from './SearchMenuItem';
 
 
 type Props = {
 type Props = {
   searchKeyword: string,
   searchKeyword: string,
+  getItemProps: any,
+  highlightedIndex: number | null,
 }
 }
 export const SearchResultMenuItem = (props: Props): JSX.Element => {
 export const SearchResultMenuItem = (props: Props): JSX.Element => {
-  const { searchKeyword } = props;
+  const { searchKeyword, highlightedIndex, getItemProps } = props;
 
 
   const debouncedKeyword = useDebounce(searchKeyword, 500);
   const debouncedKeyword = useDebounce(searchKeyword, 500);
 
 
-  const isEmptyKeyword = debouncedKeyword.trim() === '';
+  const isEmptyKeyword = searchKeyword.trim().length === 0;
 
 
-  const { data: searchResult, isLoading } = useSWRxSearch(isEmptyKeyword ? null : searchKeyword, null, { limit: 10 });
+  const { data: searchResult, isLoading } = useSWRxSearch(isEmptyKeyword ? null : debouncedKeyword, null, { limit: 10 });
+
+  const getFiexdIndex = useCallback((index: number | null) => {
+    if (index == null) {
+      return -1;
+    }
+
+    return (isEmptyKeyword ? 1 : 3) + index;
+  }, [isEmptyKeyword]);
 
 
   if (isLoading) {
   if (isLoading) {
     return (
     return (
       <>
       <>
         Searching...
         Searching...
-        <div className="border-top mt-2" />
+        <div className="border-top mt-3" />
       </>
       </>
     );
     );
   }
   }
@@ -33,27 +44,23 @@ export const SearchResultMenuItem = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <>
     <>
-      <table>
-        <tbody>
-          {searchResult.data?.map(pageWithMeta => (
-            <tr key={pageWithMeta.data._id}>
-              <div className="ps-1 mb-2 d-flex">
-                <UserPicture user={pageWithMeta.data.creator} />
-
-                <span className="ms-3 text-break text-wrap">
-                  <PagePathLabel path={pageWithMeta.data.path} />
-                </span>
-
-                <span className="ms-2 text-muted d-flex justify-content-center align-items-center">
-                  <span className="material-symbols-outlined fs-5">footprint</span>
-                  <span>{pageWithMeta.data.seenUsers.length}</span>
-                </span>
-              </div>
-            </tr>
-          ))}
-        </tbody>
-      </table>
-      <div className="border-top mb-2" />
+      {searchResult?.data
+        .map((item, index) => (
+          <SearchMenuItem key={item.data._id} index={getFiexdIndex(index)} highlightedIndex={highlightedIndex} getItemProps={getItemProps} url={item.data._id}>
+            <UserPicture user={item.data.creator} />
+
+            <span className="ms-3 text-break text-wrap">
+              <PagePathLabel path={item.data.path} />
+            </span>
+
+            <span className="ms-2 text-muted d-flex justify-content-center align-items-center">
+              <span className="material-symbols-outlined fs-5">footprint</span>
+              <span>{item.data.seenUsers.length}</span>
+            </span>
+          </SearchMenuItem>
+        ))
+      }
+      <div className="border-top mt-3" />
     </>
     </>
   );
   );
 };
 };