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

Merge pull request #5736 from weseek/feat/add-searchbar-plpages

feat: Add searchbar plpages
Yuki Takei 4 лет назад
Родитель
Сommit
5331a43537

+ 1 - 1
packages/app/resource/locales/en_US/translation.json

@@ -640,7 +640,7 @@
   "private_legacy_pages": {
     "bulk_operation": "Bulk operation",
     "convert_all_selected_pages": "Convert all to new v5 compatible format",
-    "alert_title": "You are viewing old v4 compatible private pages.",
+    "alert_title": "Old v4 compatible format private pages exist.",
     "alert_desc1": "On this page, you can select pages with the checkbox and batch convert to the new v5 compatible format from the \"Bulk operation\" button at the top of the screen.",
     "nopages_title": "Congratulations. Ready to use GROWI v5!",
     "nopages_desc1": "Now all the pages you can manage seem to be in v5 compatible format.",

+ 1 - 1
packages/app/resource/locales/ja_JP/translation.json

@@ -639,7 +639,7 @@
   "private_legacy_pages": {
     "bulk_operation": "一括操作",
     "convert_all_selected_pages": "新しい v5 互換形式に一括変換",
-    "alert_title": "古い v4 互換形式のプライベートページを表示しています",
+    "alert_title": "古い v4 互換形式のプライベートページが存在します",
     "alert_desc1": "このページでは、チェックボックスでページを選択し、画面上部の「一括操作」ボタンから新しい v5 互換形式に一括変換できます。",
     "nopages_title": "おめでとうございます。GROWI v5 を使う準備が完了しました!",
     "nopages_desc1": "今あなたが管理可能なページはすべて v5 互換形式になっているようです。",

+ 1 - 1
packages/app/resource/locales/zh_CN/translation.json

@@ -926,7 +926,7 @@
   "private_legacy_pages": {
     "bulk_operation": "批量操作",
     "convert_all_selected_pages": "全部转换为新的v5兼容格式",
-    "alert_title": "你正在查看旧的v4兼容的私人网页。",
+    "alert_title": "存在旧的v4兼容格式的私人网页。",
     "alert_desc1": "在这一页,你可以用复选框选择页面,并通过屏幕上方的批量操作按钮批量转换为新的v5兼容格式。",
     "nopages_title": "恭喜你。准备使用GROWI v5!",
     "nopages_desc1": "现在你能管理的所有页面似乎都是v5兼容的格式。",

+ 76 - 36
packages/app/src/components/PrivateLegacyPages.tsx

@@ -12,7 +12,7 @@ import AppContainer from '~/client/services/AppContainer';
 import { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
 import { toastSuccess } from '~/client/util/apiNotification';
 import {
-  useSWRxNamedQuerySearch,
+  useSWRxSearch,
 } from '~/stores/search';
 import {
   ILegacyPrivatePage, useLegacyPrivatePagesMigrationModal,
@@ -24,10 +24,15 @@ import { OperateAllControl } from './SearchPage/OperateAllControl';
 import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage2/SearchPageBase';
 import { MenuItemType } from './Common/Dropdown/PageItemControl';
 import { LegacyPrivatePagesMigrationModal } from './LegacyPrivatePagesMigrationModal';
+import SearchControl from './SearchPage/SearchControl';
+import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
+import { V5MigrationStatus } from '~/interfaces/page-listing-results';
 
 
 // TODO: replace with "customize:showPageLimitationS"
-const INITIAL_PAGIONG_SIZE = 20;
+const INITIAL_PAGING_SIZE = 20;
+
+const initQ = '/';
 
 
 /**
@@ -39,6 +44,7 @@ type SearchResultListHeadProps = {
   offset: number,
   pagingSize: number,
   onPagingSizeChanged: (size: number) => void,
+  migrationStatus?: V5MigrationStatus,
 }
 
 const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.Element => {
@@ -46,14 +52,24 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
 
   const {
     searchResult, offset, pagingSize,
-    onPagingSizeChanged,
+    onPagingSizeChanged, migrationStatus,
   } = props;
 
+  if (migrationStatus == null) {
+    return (
+      <div className="mw-0 flex-grow-1 flex-basis-0 m-5 text-muted text-center">
+        <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+      </div>
+    );
+  }
+
   const { took, total, hitsCount } = searchResult.meta;
   const leftNum = offset + 1;
   const rightNum = offset + hitsCount;
 
-  if (total === 0) {
+  const isSuccess = migrationStatus.migratablePagesCount === 0;
+
+  if (isSuccess) {
     return (
       <div className="card border-success mt-3">
         <div className="card-body">
@@ -125,19 +141,30 @@ export const PrivateLegacyPages = (props: Props): JSX.Element => {
   } = props;
 
 
+  const [keyword, setKeyword] = useState<string>(initQ);
   const [offset, setOffset] = useState<number>(0);
-  const [limit, setLimit] = useState<number>(INITIAL_PAGIONG_SIZE);
+  const [limit, setLimit] = useState<number>(INITIAL_PAGING_SIZE);
 
   const [isControlEnabled, setControlEnabled] = useState(false);
 
   const selectAllControlRef = useRef<ISelectableAndIndeterminatable|null>(null);
   const searchPageBaseRef = useRef<ISelectableAll & IReturnSelectedPageIds|null>(null);
 
-  const { data, conditions, mutate } = useSWRxNamedQuerySearch('PrivateLegacyPages', {
+  const { data, conditions, mutate } = useSWRxSearch(keyword, 'PrivateLegacyPages', {
     offset,
     limit,
+    includeUserPages: true,
+    includeTrashPages: false,
   });
 
+  const { data: migrationStatus, mutate: mutateMigrationStatus } = useSWRxV5MigrationStatus();
+
+  const searchInvokedHandler = useCallback((_keyword: string) => {
+    mutateMigrationStatus();
+    setKeyword(_keyword);
+    setOffset(0);
+  }, []);
+
   const { open: openModal, close: closeModal } = useLegacyPrivatePagesMigrationModal();
 
   const selectAllCheckboxChangedHandler = useCallback((isChecked: boolean) => {
@@ -206,10 +233,11 @@ export const PrivateLegacyPages = (props: Props): JSX.Element => {
       () => {
         toastSuccess(t('Successfully requested'));
         closeModal();
+        mutateMigrationStatus();
         mutate();
       },
     );
-  }, [data, mutate, openModal, closeModal]);
+  }, [data, mutate, openModal, closeModal, mutateMigrationStatus]);
 
   const pagingSizeChangedHandler = useCallback((pagingSize: number) => {
     setOffset(0);
@@ -224,42 +252,53 @@ export const PrivateLegacyPages = (props: Props): JSX.Element => {
 
   const hitsCount = data?.meta.hitsCount;
 
-  const searchControl = useMemo(() => {
+  const searchControlAllAction = useMemo(() => {
     const isCheckboxDisabled = hitsCount === 0;
 
     return (
-      <div className="shadow-sm">
-        <div className="search-control d-flex align-items-center py-md-2 py-3 px-md-4 px-3 border-bottom border-gray">
-          <div className="d-flex pl-md-2">
-            <OperateAllControl
-              ref={selectAllControlRef}
-              isCheckboxDisabled={isCheckboxDisabled}
-              onCheckboxChanged={selectAllCheckboxChangedHandler}
-            >
-              <UncontrolledButtonDropdown>
-                <DropdownToggle caret color="outline-primary" disabled={!isControlEnabled}>
-                  {t('private_legacy_pages.bulk_operation')}
-                </DropdownToggle>
-                <DropdownMenu>
-                  <DropdownItem onClick={convertMenuItemClickedHandler}>
-                    <i className="icon-fw icon-refresh"></i>
-                    {t('private_legacy_pages.convert_all_selected_pages')}
-                  </DropdownItem>
-                  <DropdownItem onClick={deleteAllButtonClickedHandler}>
-                    <span className="text-danger">
-                      <i className="icon-fw icon-trash"></i>
-                      {t('search_result.delete_all_selected_page')}
-                    </span>
-                  </DropdownItem>
-                </DropdownMenu>
-              </UncontrolledButtonDropdown>
-            </OperateAllControl>
-          </div>
+      <div className="search-control d-flex align-items-center">
+        <div className="d-flex pl-md-2">
+          <OperateAllControl
+            ref={selectAllControlRef}
+            isCheckboxDisabled={isCheckboxDisabled}
+            onCheckboxChanged={selectAllCheckboxChangedHandler}
+          >
+            <UncontrolledButtonDropdown>
+              <DropdownToggle caret color="outline-primary" disabled={!isControlEnabled}>
+                {t('private_legacy_pages.bulk_operation')}
+              </DropdownToggle>
+              <DropdownMenu>
+                <DropdownItem onClick={convertMenuItemClickedHandler}>
+                  <i className="icon-fw icon-refresh"></i>
+                  {t('private_legacy_pages.convert_all_selected_pages')}
+                </DropdownItem>
+                <DropdownItem onClick={deleteAllButtonClickedHandler}>
+                  <span className="text-danger">
+                    <i className="icon-fw icon-trash"></i>
+                    {t('search_result.delete_all_selected_page')}
+                  </span>
+                </DropdownItem>
+              </DropdownMenu>
+            </UncontrolledButtonDropdown>
+          </OperateAllControl>
         </div>
       </div>
     );
   }, [convertMenuItemClickedHandler, deleteAllButtonClickedHandler, hitsCount, isControlEnabled, selectAllCheckboxChangedHandler, t]);
 
+  const searchControl = useMemo(() => {
+    return (
+      <SearchControl
+        isSearchServiceReachable
+        isEnableSort={false}
+        isEnableFilter={false}
+        initialSearchConditions={{ keyword: initQ, limit: INITIAL_PAGING_SIZE }}
+        onSearchInvoked={searchInvokedHandler}
+        allControl={searchControlAllAction}
+      />
+    );
+  }, [searchInvokedHandler, searchControlAllAction]);
+
   const searchResultListHead = useMemo(() => {
     if (data == null) {
       return <></>;
@@ -270,9 +309,10 @@ export const PrivateLegacyPages = (props: Props): JSX.Element => {
         offset={offset}
         pagingSize={limit}
         onPagingSizeChanged={pagingSizeChangedHandler}
+        migrationStatus={migrationStatus}
       />
     );
-  }, [data, limit, offset, pagingSizeChangedHandler]);
+  }, [data, limit, offset, pagingSizeChangedHandler, migrationStatus]);
 
   const searchPager = useMemo(() => {
     // when pager is not needed

+ 7 - 5
packages/app/src/components/SearchPage.tsx

@@ -9,7 +9,7 @@ import AppContainer from '~/client/services/AppContainer';
 import { IFormattedSearchResult } from '~/interfaces/search';
 import { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
 import { useIsSearchServiceReachable } from '~/stores/context';
-import { ISearchConditions, ISearchConfigurations, useSWRxFullTextSearch } from '~/stores/search';
+import { ISearchConditions, ISearchConfigurations, useSWRxSearch } from '~/stores/search';
 
 import PaginationWrapper from './PaginationWrapper';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
@@ -119,7 +119,7 @@ export const SearchPage = (props: Props): JSX.Element => {
 
   const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
 
-  const { data, conditions, mutate } = useSWRxFullTextSearch(keyword, {
+  const { data, conditions, mutate } = useSWRxSearch(keyword, null, {
     ...configurationsByControl,
     offset,
     limit,
@@ -193,7 +193,7 @@ export const SearchPage = (props: Props): JSX.Element => {
   }, [keyword]);
   const hitsCount = data?.meta.hitsCount;
 
-  const deleteAllControl = useMemo(() => {
+  const allControl = useMemo(() => {
     const isDisabled = hitsCount === 0;
 
     return (
@@ -222,13 +222,15 @@ export const SearchPage = (props: Props): JSX.Element => {
     return (
       <SearchControl
         isSearchServiceReachable={isSearchServiceReachable}
+        isEnableSort
+        isEnableFilter
         initialSearchConditions={initialSearchConditions}
         onSearchInvoked={searchInvokedHandler}
-        deleteAllControl={deleteAllControl}
+        allControl={allControl}
       >
       </SearchControl>
     );
-  }, [deleteAllControl, initialSearchConditions, isSearchServiceReachable, searchInvokedHandler]);
+  }, [allControl, initialSearchConditions, isSearchServiceReachable, searchInvokedHandler]);
 
   const searchResultListHead = useMemo(() => {
     if (data == null) {

+ 70 - 58
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -12,20 +12,24 @@ import SearchForm from '../SearchForm';
 
 type Props = {
   isSearchServiceReachable: boolean,
+  isEnableSort: boolean,
+  isEnableFilter: boolean,
   initialSearchConditions: Partial<ISearchConditions>,
 
   onSearchInvoked: (keyword: string, configurations: Partial<ISearchConfigurations>) => void,
 
-  deleteAllControl: React.ReactNode,
+  allControl: React.ReactNode,
 }
 
 const SearchControl: FC <Props> = React.memo((props: Props) => {
 
   const {
     isSearchServiceReachable,
+    isEnableSort,
+    isEnableFilter,
     initialSearchConditions,
     onSearchInvoked,
-    deleteAllControl,
+    allControl,
   } = props;
 
   const [keyword, setKeyword] = useState(initialSearchConditions.keyword ?? '');
@@ -73,71 +77,79 @@ const SearchControl: FC <Props> = React.memo((props: Props) => {
         </div>
 
         {/* sort option: show when screen is larger than lg */}
-        <div className="mr-4 d-lg-flex d-none">
-          <SortControl
-            sort={sort}
-            order={order}
-            onChange={changeSortHandler}
-          />
-        </div>
+        {isEnableSort && (
+          <div className="mr-4 d-lg-flex d-none">
+            <SortControl
+              sort={sort}
+              order={order}
+              onChange={changeSortHandler}
+            />
+          </div>
+        )}
       </div>
       {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
       <div className="search-control d-flex align-items-center py-md-2 py-3 px-md-4 px-3 border-bottom border-gray">
         <div className="d-flex">
-          {deleteAllControl}
+          {allControl}
         </div>
         {/* sort option: show when screen is smaller than lg */}
-        <div className="mr-md-4 mr-2 d-flex d-lg-none ml-auto">
-          <SortControl
-            sort={sort}
-            order={order}
-            onChange={changeSortHandler}
-          />
-        </div>
-        {/* filter option */}
-        <div className="d-lg-none">
-          <button
-            type="button"
-            className="btn"
-            onClick={() => setIsFileterOptionModalShown(true)}
-          >
-            <i className="icon-equalizer"></i>
-          </button>
-        </div>
-        <div className="d-none d-lg-flex align-items-center ml-auto search-control-include-options">
-          <div className="border rounded px-2 py-1 mr-3">
-            <div className="custom-control custom-checkbox custom-checkbox-primary">
-              <input
-                className="custom-control-input mr-2"
-                type="checkbox"
-                id="flexCheckDefault"
-                defaultChecked={includeUserPages}
-                onChange={e => setIncludeUserPages(e.target.checked)}
-              />
-              <label className="custom-control-label mb-0 d-flex align-items-center text-secondary with-no-font-weight" htmlFor="flexCheckDefault">
-                {t('Include Subordinated Target Page', { target: '/user' })}
-              </label>
-            </div>
+        {isEnableSort && (
+          <div className="mr-md-4 mr-2 d-flex d-lg-none ml-auto">
+            <SortControl
+              sort={sort}
+              order={order}
+              onChange={changeSortHandler}
+            />
           </div>
-          <div className="border rounded px-2 py-1">
-            <div className="custom-control custom-checkbox custom-checkbox-primary">
-              <input
-                className="custom-control-input mr-2"
-                type="checkbox"
-                id="flexCheckChecked"
-                checked={includeTrashPages}
-                onChange={e => setIncludeTrashPages(e.target.checked)}
-              />
-              <label
-                className="custom-control-label
-              d-flex align-items-center text-secondary with-no-font-weight"
-                htmlFor="flexCheckChecked"
+        )}
+        {/* filter option */}
+        {isEnableFilter && (
+          <>
+            <div className="d-lg-none">
+              <button
+                type="button"
+                className="btn"
+                onClick={() => setIsFileterOptionModalShown(true)}
               >
-                {t('Include Subordinated Target Page', { target: '/trash' })}
-              </label>
+                <i className="icon-equalizer"></i>
+              </button>
             </div>
-          </div>
-        </div>
+            <div className="d-none d-lg-flex align-items-center ml-auto search-control-include-options">
+              <div className="border rounded px-2 py-1 mr-3">
+                <div className="custom-control custom-checkbox custom-checkbox-primary">
+                  <input
+                    className="custom-control-input mr-2"
+                    type="checkbox"
+                    id="flexCheckDefault"
+                    defaultChecked={includeUserPages}
+                    onChange={e => setIncludeUserPages(e.target.checked)}
+                  />
+                  <label className="custom-control-label mb-0 d-flex align-items-center text-secondary with-no-font-weight" htmlFor="flexCheckDefault">
+                    {t('Include Subordinated Target Page', { target: '/user' })}
+                  </label>
+                </div>
+              </div>
+              <div className="border rounded px-2 py-1">
+                <div className="custom-control custom-checkbox custom-checkbox-primary">
+                  <input
+                    className="custom-control-input mr-2"
+                    type="checkbox"
+                    id="flexCheckChecked"
+                    checked={includeTrashPages}
+                    onChange={e => setIncludeTrashPages(e.target.checked)}
+                  />
+                  <label
+                    className="custom-control-label
+                  d-flex align-items-center text-secondary with-no-font-weight"
+                    htmlFor="flexCheckChecked"
+                  >
+                    {t('Include Subordinated Target Page', { target: '/trash' })}
+                  </label>
+                </div>
+              </div>
+            </div>
+          </>
+        )}
       </div>
 
       <SearchOptionModal

+ 3 - 2
packages/app/src/components/SearchTypeahead.tsx

@@ -11,7 +11,7 @@ import { IFocusable } from '~/client/interfaces/focusable';
 import { TypeaheadProps } from '~/client/interfaces/react-bootstrap-typeahead';
 import { IPageSearchMeta } from '~/interfaces/search';
 import { IPageWithMeta } from '~/interfaces/page';
-import { useSWRxFullTextSearch } from '~/stores/search';
+import { useSWRxSearch } from '~/stores/search';
 
 
 type ResetFormButtonProps = {
@@ -61,8 +61,9 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
   const [searchKeyword, setSearchKeyword] = useState('');
   const [isForcused, setFocused] = useState(false);
 
-  const { data: searchResult, error: searchError } = useSWRxFullTextSearch(
+  const { data: searchResult, error: searchError } = useSWRxSearch(
     disableIncrementalSearch ? null : searchKeyword,
+    null,
     { limit: 10 },
   );
 

+ 2 - 0
packages/app/src/server/service/search-delegator/private-legacy-pages.ts

@@ -48,10 +48,12 @@ class PrivateLegacyPagesDelegator implements SearchDelegator<IPage, MongoTermsKe
     const _pages: PageDocument[] = await findQueryBuilder
       .addConditionToPagenate(offset, limit)
       .query
+      .populate('creator')
       .populate('lastUpdateUser')
       .exec();
 
     const pages = _pages.map((page) => {
+      page.creator = serializeUserSecurely(page.creator);
       page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
       return page;
     });

+ 1 - 1
packages/app/src/stores/page-listing.tsx

@@ -57,7 +57,7 @@ export const useSWRxPageChildren = (
 
 export const useSWRxV5MigrationStatus = (
 ): SWRResponse<V5MigrationStatus, Error> => {
-  return useSWRImmutable(
+  return useSWR(
     '/pages/v5-migration-status',
     endpoint => apiv3Get(endpoint).then((response) => {
       return {

+ 3 - 15
packages/app/src/stores/search.tsx

@@ -50,8 +50,8 @@ const createSearchQuery = (keyword: string, includeTrashPages: boolean, includeU
   return query;
 };
 
-export const useSWRxFullTextSearch = (
-    keyword: string | null, configurations: ISearchConfigurations, disableTermManager = false,
+export const useSWRxSearch = (
+    keyword: string | null, nqName: string | null, configurations: ISearchConfigurations, disableTermManager = false,
 ): SWRResponse<IFormattedSearchResult, Error> & { conditions: ISearchConditions } => {
   const { data: termNumber } = useFullTextSearchTermManager(disableTermManager);
 
@@ -81,6 +81,7 @@ export const useSWRxFullTextSearch = (
       return apiGet(
         endpoint, {
           q: encodeURIComponent(rawQuery),
+          nq: typeof nqName === 'string' ? encodeURIComponent(nqName) : null,
           limit,
           offset,
           sort,
@@ -100,16 +101,3 @@ export const useSWRxFullTextSearch = (
     },
   };
 };
-
-export const useSWRxNamedQuerySearch = (
-    namedQuery: string, configurations: ISearchConfigurations,
-): SWRResponse<IFormattedSearchResult, Error> & { conditions: ISearchConditions } => {
-
-  const keyword = `[nq:${namedQuery}]`;
-  return useSWRxFullTextSearch(keyword, {
-    ...configurations,
-    includeTrashPages: true,
-    includeUserPages: true,
-  });
-
-};