瀏覽代碼

Merge pull request #5646 from weseek/feat/gw7758-reimplementation-api-v3-pages-recent

feat: gw7758 reimplementation api v3 pages recent
Mudana-Grune 4 年之前
父節點
當前提交
47e5b533fc

+ 60 - 0
packages/app/src/components/Sidebar/InfiniteScroll.tsx

@@ -0,0 +1,60 @@
+import React, {
+  Ref, useEffect, useState,
+} from 'react';
+import type { SWRInfiniteResponse } from 'swr/infinite';
+
+type Props<T> = {
+  swr: SWRInfiniteResponse<T>
+  children: React.ReactChild | ((item: T) => React.ReactNode),
+  loadingIndicator?: React.ReactNode
+  endingIndicator?: React.ReactNode
+  isReachingEnd?: boolean,
+  offset?: number
+}
+
+const useIntersection = <T extends HTMLElement>(): [boolean, Ref<T>] => {
+  const [intersecting, setIntersecting] = useState<boolean>(false);
+  const [element, setElement] = useState<HTMLElement>();
+  useEffect(() => {
+    if (!element) return;
+    const observer = new IntersectionObserver((entries) => {
+      setIntersecting(entries[0]?.isIntersecting);
+    });
+    observer.observe(element);
+    return () => observer.unobserve(element);
+  }, [element]);
+  return [intersecting, el => el && setElement(el)];
+};
+
+const InfiniteScroll = <T, >(props: Props<T>): React.ReactElement<Props<T>> => {
+  const {
+    swr: {
+      setSize, data, isValidating,
+    },
+    children,
+    loadingIndicator,
+    endingIndicator,
+    isReachingEnd,
+    offset = 0,
+  } = props;
+
+  const [intersecting, ref] = useIntersection<HTMLDivElement>();
+
+  useEffect(() => {
+    if (intersecting && !isValidating && !isReachingEnd) {
+      setSize(size => size + 1);
+    }
+  }, [setSize, intersecting]);
+
+  return (
+    <>
+      {typeof children === 'function' ? data?.map(item => children(item)) : children}
+      <div style={{ position: 'relative' }}>
+        <div ref={ref} style={{ position: 'absolute', top: offset }}></div>
+        {isReachingEnd ? endingIndicator : loadingIndicator}
+      </div>
+    </>
+  );
+};
+
+export default InfiniteScroll;

+ 26 - 11
packages/app/src/components/Sidebar/RecentChanges.tsx

@@ -10,11 +10,11 @@ import { UserPicture, FootstampIcon } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
 
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
-import { useSWRxRecentlyUpdated } from '~/stores/page';
+import { useSWRInifinitexRecentlyUpdated } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 import LinkedPagePath from '~/models/linked-page-path';
-
+import InfiniteScroll from './InfiniteScroll';
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 
@@ -120,14 +120,21 @@ SmallPageItem.propTypes = {
   page: PropTypes.any,
 };
 
+function LoadingIndicator() {
+  return (
+    <div className="text-muted text-center">
+      <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+    </div>
+  );
+}
 
 const RecentChanges = (): JSX.Element => {
-
+  const PER_PAGE = 20;
   const { t } = useTranslation();
-  const { data: pages, mutate } = useSWRxRecentlyUpdated();
-
+  const swr = useSWRInifinitexRecentlyUpdated();
   const [isRecentChangesSidebarSmall, setIsRecentChangesSidebarSmall] = useState(false);
-
+  const isEmpty = swr.data?.[0].length === 0;
+  const isReachingEnd = isEmpty || (swr.data && swr.data[swr.data.length - 1]?.length < PER_PAGE);
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
     if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
       setIsRecentChangesSidebarSmall(true);
@@ -148,7 +155,7 @@ const RecentChanges = (): JSX.Element => {
     <>
       <div className="grw-sidebar-content-header p-3 d-flex">
         <h3 className="mb-0  text-nowrap">{t('Recent Changes')}</h3>
-        <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
+        <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => swr.mutate()}>
           <i className="icon icon-reload"></i>
         </button>
         <div className="d-flex align-items-center">
@@ -167,14 +174,22 @@ const RecentChanges = (): JSX.Element => {
       </div>
       <div className="grw-recent-changes p-3">
         <ul className="list-group list-group-flush">
-          {(pages || []).map(page => (isRecentChangesSidebarSmall
-            ? <SmallPageItem key={page._id} page={page} />
-            : <LargePageItem key={page._id} page={page} />))}
+          <InfiniteScroll
+            swr={swr}
+            loadingIndicator={<LoadingIndicator />}
+            isReachingEnd={isReachingEnd}
+          >
+            {pages => pages.map(page => (
+              isRecentChangesSidebarSmall
+                ? <SmallPageItem key={page._id} page={page} />
+                : <LargePageItem key={page._id} page={page} />
+            ))
+            }
+          </InfiniteScroll>
         </ul>
       </div>
     </>
   );
 
 };
-
 export default RecentChanges;

+ 17 - 7
packages/app/src/server/models/obsolete-page.js

@@ -506,7 +506,7 @@ export const getPageSchema = (crowi) => {
     validateCrowi();
 
     const User = crowi.model('User');
-
+    const Page = crowi.model('Page');
     const opt = Object.assign({ sort: 'updatedAt', desc: -1 }, option);
     const sortOpt = {};
     sortOpt[opt.sort] = opt.desc;
@@ -523,13 +523,23 @@ export const getPageSchema = (crowi) => {
     const totalCount = await builder.query.exec('count');
 
     // find
-    builder.addConditionToPagenate(opt.offset, opt.limit, sortOpt);
-    builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
-    const pages = await builder.query.lean().clone().exec('find');
-
-    const result = {
-      pages, totalCount, offset: opt.offset, limit: opt.limit,
+    // builder.addConditionToPagenate(opt.offset, opt.limit, sortOpt);
+
+    const customLabels = {
+      totalDocs: 'totalCount',
+      docs: 'pages',
+      limit: 'limit',
+      page: 'currentPage',
+      nextPage: 'nextPage',
+      prevPage: 'prevPage',
+      totalPages: 'pageCount',
+    };
+    const paginationOptions = {
+      lean: true, limit: opt.limit, offset: opt.offset, page: opt.page, customLabels, sort: sortOpt,
     };
+    builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
+    const pages = await Page.paginate(builder.query, paginationOptions);
+    const result = { ...pages, offset: opt.offset };
     return result;
   }
 

+ 4 - 3
packages/app/src/server/routes/apiv3/pages.js

@@ -365,8 +365,9 @@ module.exports = (crowi) => {
    */
   router.get('/recent', accessTokenParser, loginRequired, async(req, res) => {
     const limit = 20;
-    const offset = parseInt(req.query.offset) || 0;
-
+    // const offset = parseInt(req.query.offset) || 0;
+    const page = parseInt(req.query.page) || 1;
+    const offset = (+page - 1) * limit;
     const queryOptions = {
       offset,
       limit,
@@ -374,8 +375,8 @@ module.exports = (crowi) => {
       isRegExpEscapedFromPath: true,
       sort: 'updatedAt',
       desc: -1,
+      page,
     };
-
     try {
       const result = await Page.findListWithDescendants('/', req.user, queryOptions);
       if (result.pages.length > limit) {

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

@@ -1,4 +1,5 @@
 import useSWR, { SWRResponse } from 'swr';
+import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite';
 import useSWRImmutable from 'swr/immutable';
 
 import { apiv3Get } from '~/client/util/apiv3-client';
@@ -33,7 +34,19 @@ export const useSWRxRecentlyUpdated = (): SWRResponse<(IPageHasId)[], Error> =>
     endpoint => apiv3Get<{ pages:(IPageHasId)[] }>(endpoint).then(response => response.data?.pages),
   );
 };
-
+export const useSWRInifinitexRecentlyUpdated = () : SWRInfiniteResponse<(IPageHasId)[], Error> => {
+  const getKey = (page: number) => {
+    return `/pages/recent?page=${page + 1}`;
+  };
+  return useSWRInfinite(
+    getKey,
+    (endpoint: string) => apiv3Get<{ pages:(IPageHasId)[] }>(endpoint).then(response => response.data?.pages),
+    {
+      revalidateFirstPage: false,
+      revalidateAll: false,
+    },
+  );
+};
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 export const useSWRxPageList = (path: string | null, pageNumber?: number, termNumber?: number): SWRResponse<IPagingResult<IPageHasId>, Error> => {