Explorar o código

add PrivateLegacyPages

Yuki Takei %!s(int64=4) %!d(string=hai) anos
pai
achega
223dafe1b2

+ 3 - 0
packages/app/src/client/app.jsx

@@ -54,6 +54,7 @@ import PersonalContainer from '~/client/services/PersonalContainer';
 
 import { appContainer, componentMappings } from './base';
 import { toastError } from './util/apiNotification';
+import { PrivateLegacyPages } from '~/components/PrivateLegacyPages';
 
 const logger = loggerFactory('growi:cli:app');
 
@@ -86,6 +87,8 @@ Object.assign(componentMappings, {
   'grw-sidebar-wrapper': <Sidebar />,
 
   'search-page': <SearchPage appContainer={appContainer} />,
+  'private-regacy-pages': <PrivateLegacyPages appContainer={appContainer} />,
+
   'all-in-app-notifications': <InAppNotificationPage />,
   'identical-path-page': <IdenticalPathPage />,
 

+ 266 - 0
packages/app/src/components/PrivateLegacyPages.tsx

@@ -0,0 +1,266 @@
+import React, {
+  useCallback, useMemo, useRef, useState,
+} from 'react';
+import { useTranslation } from 'react-i18next';
+
+import AppContainer from '~/client/services/AppContainer';
+import { IFormattedSearchResult } from '~/interfaces/search';
+import { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
+import { useIsSearchServiceConfigured, useIsSearchServiceReachable } from '~/stores/context';
+import {
+  ISearchConfigurations, useSWRxNamedQuerySearch,
+} from '~/stores/search';
+
+import PaginationWrapper from './PaginationWrapper';
+import { OperateAllControl } from './SearchPage/OperateAllControl';
+
+import { SearchPageBase } from './SearchPage2/SearchPageBase';
+
+
+// TODO: replace with "customize:showPageLimitationS"
+const INITIAL_PAGIONG_SIZE = 20;
+
+
+/**
+ * SearchResultListHead
+ */
+
+type SearchResultListHeadProps = {
+  searchResult: IFormattedSearchResult,
+  offset: number,
+  pagingSize: number,
+  onPagingSizeChanged: (size: number) => void,
+}
+
+const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const {
+    searchResult, offset, pagingSize,
+    onPagingSizeChanged,
+  } = props;
+
+  const { took, total, hitsCount } = searchResult.meta;
+  const leftNum = offset + 1;
+  const rightNum = offset + hitsCount;
+
+  if (total === 0) {
+    return <div className="alert alert-success mt-3">(TBD) zero</div>;
+  }
+
+  return (
+    <>
+      <div className="form-inline d-flex align-items-center justify-content-between">
+        <div className="text-nowrap">
+          {t('search_result.result_meta')}
+          <span className="ml-3">{`${leftNum}-${rightNum}`} / {total}</span>
+          { took != null && (
+            <span className="ml-3 text-muted">({took}ms)</span>
+          ) }
+        </div>
+        <div className="input-group flex-nowrap search-result-select-group ml-auto d-md-flex d-none">
+          <div className="input-group-prepend">
+            <label className="input-group-text text-muted" htmlFor="inputGroupSelect01">{t('search_result.number_of_list_to_display')}</label>
+          </div>
+          <select
+            defaultValue={pagingSize}
+            className="custom-select"
+            id="inputGroupSelect01"
+            onChange={e => onPagingSizeChanged(Number(e.target.value))}
+          >
+            {[20, 50, 100, 200].map((limit) => {
+              return <option key={limit} value={limit}>{limit} {t('search_result.page_number_unit')}</option>;
+            })}
+          </select>
+        </div>
+      </div>
+      <div className="alert alert-warning mt-3">(TBD) warning</div>
+    </>
+  );
+});
+
+
+/**
+ * LegacyPage
+ */
+
+type Props = {
+  appContainer: AppContainer,
+}
+
+export const PrivateLegacyPages = (props: Props): JSX.Element => {
+  const { t } = useTranslation();
+
+  const {
+    appContainer,
+  } = props;
+
+
+  const [configurationsByPagination, setConfigurationsByPagination] = useState<Partial<ISearchConfigurations>>({
+    limit: INITIAL_PAGIONG_SIZE,
+  });
+
+  const selectAllControlRef = useRef<ISelectableAndIndeterminatable|null>(null);
+  const searchPageBaseRef = useRef<ISelectableAll|null>(null);
+
+  const { data: isSearchServiceConfigured } = useIsSearchServiceConfigured();
+  const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
+
+  const { data, conditions } = useSWRxNamedQuerySearch('PrivateLegacyPages', {
+    limit: INITIAL_PAGIONG_SIZE,
+    ...configurationsByPagination,
+    includeUserPages: true,
+  });
+
+  const selectAllCheckboxChangedHandler = useCallback((isChecked: boolean) => {
+    const instance = searchPageBaseRef.current;
+
+    if (instance == null) {
+      return;
+    }
+
+    if (isChecked) {
+      instance.selectAll();
+    }
+    else {
+      instance.deselectAll();
+    }
+  }, []);
+
+  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 {
+      instance.setIndeterminate();
+    }
+  }, []);
+
+  const pagingNumberChangedHandler = useCallback((activePage: number) => {
+    const currentLimit = configurationsByPagination.limit ?? INITIAL_PAGIONG_SIZE;
+    setConfigurationsByPagination({
+      ...configurationsByPagination,
+      offset: (activePage - 1) * currentLimit,
+    });
+  }, [configurationsByPagination]);
+
+  const hitsCount = data?.meta.hitsCount;
+
+  const { offset, limit } = conditions;
+
+  const convertAllControl = useMemo(() => {
+    const isDisabled = hitsCount === 0;
+
+    return (
+      <OperateAllControl
+        ref={selectAllControlRef}
+        isCheckboxDisabled={isDisabled}
+        onCheckboxChanged={selectAllCheckboxChangedHandler}
+      >
+        <button
+          type="button"
+          className="btn btn-outline-danger border-0 px-2"
+          disabled={isDisabled}
+          onClick={() => null /* TODO implement */}
+        >
+          <i className="icon-fw icon-trash"></i>
+          {t('search_result.delete_all_selected_page')}
+        </button>
+      </OperateAllControl>
+    );
+  }, [hitsCount, selectAllCheckboxChangedHandler, t]);
+
+  const searchControl = useMemo(() => {
+    if (!isSearchServiceReachable) {
+      return <></>;
+    }
+    return (
+      <div className="position-sticky fixed-top 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">
+            {convertAllControl}
+          </div>
+        </div>
+      </div>
+    );
+  }, [convertAllControl, isSearchServiceReachable]);
+
+  const searchResultListHead = useMemo(() => {
+    if (data == null) {
+      return <></>;
+    }
+    return (
+      <SearchResultListHead
+        searchResult={data}
+        offset={offset}
+        pagingSize={limit}
+        onPagingSizeChanged={() => {}}
+      />
+    );
+  }, [data, limit, offset]);
+
+  const searchPager = useMemo(() => {
+    // when pager is not needed
+    if (data == null || data.meta.hitsCount === data.meta.total) {
+      return <></>;
+    }
+
+    const { total } = data.meta;
+    const { offset, limit } = conditions;
+
+    return (
+      <PaginationWrapper
+        activePage={Math.floor(offset / limit) + 1}
+        totalItemsCount={total}
+        pagingLimit={configurationsByPagination?.limit}
+        changePage={pagingNumberChangedHandler}
+      />
+    );
+  }, [conditions, configurationsByPagination?.limit, data, pagingNumberChangedHandler]);
+
+  if (!isSearchServiceConfigured) {
+    return (
+      <div className="grw-container-convertible">
+        <div className="row mt-5">
+          <div className="col text-muted">
+            <h1>Search service is not configured in this system.</h1>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  if (!isSearchServiceReachable) {
+    return (
+      <div className="grw-container-convertible">
+        <div className="row mt-5">
+          <div className="col text-muted">
+            <h1>Search service occures errors. Please contact to administrators of this system.</h1>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <SearchPageBase
+      ref={searchPageBaseRef}
+      appContainer={appContainer}
+      pages={data?.data}
+      onSelectedPagesByCheckboxesChanged={selectedPagesByCheckboxesChangedHandler}
+      // Components
+      searchControl={searchControl}
+      searchResultListHead={searchResultListHead}
+      searchPager={searchPager}
+    />
+  );
+};

+ 2 - 2
packages/app/src/components/Sidebar/PageTree.tsx

@@ -7,7 +7,7 @@ import {
 } from '~/stores/context';
 
 import ItemsTree from './PageTree/ItemsTree';
-import PrivateLegacyPages from './PageTree/PrivateLegacyPages';
+import { PrivateLegacyPagesLink } from './PageTree/PrivateLegacyPagesLink';
 
 const PageTree: FC = memo(() => {
   const { t } = useTranslation();
@@ -76,7 +76,7 @@ const PageTree: FC = memo(() => {
 
       {!isGuestUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
         <div className="grw-pagetree-footer border-top p-3 w-100">
-          <PrivateLegacyPages />
+          <PrivateLegacyPagesLink />
         </div>
       )}
     </>

+ 2 - 4
packages/app/src/components/Sidebar/PageTree/PrivateLegacyPages.tsx → packages/app/src/components/Sidebar/PageTree/PrivateLegacyPagesLink.tsx

@@ -1,14 +1,12 @@
 import React, { FC, memo } from 'react';
 import { useTranslation } from 'react-i18next';
 
-const PrivateLegacyPages: FC = memo(() => {
+export const PrivateLegacyPagesLink: FC = memo(() => {
   const { t } = useTranslation();
 
   return (
-    <a href="/private-legacy-pages?q=[nq:PrivateLegacyPages]" className="h5 grw-private-legacy-pages-anchor text-decoration-none">
+    <a href="/private-legacy-pages" className="h5 grw-private-legacy-pages-anchor text-decoration-none">
       <i className="icon-drawer mr-2"></i> {t('pagetree.private_legacy_pages')}
     </a>
   );
 });
-
-export default PrivateLegacyPages;

+ 8 - 1
packages/app/src/server/views/private-legacy-pages.html

@@ -11,6 +11,13 @@
   data-target="#search-result-list"
 {% endblock %}
 
+<!-- add .on-search to body tag class in layout -->
+{% set additionalBodyClass = 'on-search' %}
+
 {% block layout_main %}
-<p>This page is not implemented.</p>
+<div id="grw-fav-sticky-trigger" class="sticky-top"></div>
+
+<div id="main" class="main search-page mt-0">
+  <div id="private-regacy-pages"></div>
+</div>
 {% endblock %} {# layout_main #}

+ 9 - 0
packages/app/src/stores/search.tsx

@@ -90,3 +90,12 @@ export const useSWRxFullTextSearch = (
     },
   };
 };
+
+export const useSWRxNamedQuerySearch = (
+    namedQuery: string, configurations: ISearchConfigurations,
+): SWRResponse<IFormattedSearchResult, Error> & { conditions: ISearchConditions } => {
+
+  const keyword = `[nq:${namedQuery}]`;
+  return useSWRxFullTextSearch(keyword, configurations);
+
+};