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

Show bookmarks list on the sidebar

https://youtrack.weseek.co.jp/issue/GW-7832
- Add offset option to user's bookmarks route
- Implement SWRInfinite to fetch user's bookmarks list
- Modify useCurrentUser return type to IUserHasId
- Implement Infinite scroll to bookmarks list on side bar
- Add styling for bookmark list
mudana 3 лет назад
Родитель
Сommit
563e02b061

+ 59 - 7
packages/app/src/components/Sidebar/Bookmarks.tsx

@@ -1,16 +1,68 @@
-import React, { FC } from 'react';
 
+import React from 'react';
+
+import { DevidedPagePath } from '@growi/core';
+import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 
-const Bookmarks: FC = () => {
-  const { t } = useTranslation('');
+import LinkedPagePath from '~/models/linked-page-path';
+import { useSWRInifiniteBookmarkedPage } from '~/stores/bookmark';
+import { useCurrentUser } from '~/stores/context';
+
+import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
+
+import InfiniteScroll from './InfiniteScroll';
+
+
+const BookmarksItem = ({ data }) => {
+  const { page } = data;
+  const dPagePath = new DevidedPagePath(page.path, false, true);
+  const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+
+  return (
+    <li className="list-group-item py-3 px-0">
+      <div className="d-flex w-100">
+        <h5 className="my-0">
+          <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
+        </h5>
+      </div>
+    </li>
+  );
+
+};
 
-  // TODO Add bookmarks item
+BookmarksItem.propTypes = {
+  data: PropTypes.any,
+};
+
+const Bookmarks = () : JSX.Element => {
+  const { t } = useTranslation('');
+  const { data: currentUser } = useCurrentUser();
+  const swr = useSWRInifiniteBookmarkedPage(currentUser?._id);
+  const isEmpty = swr.data?.[0].docs.length === 0;
+  const isReachingEnd = isEmpty || (swr.data && swr.data[0].nextPage == null);
 
   return (
-    <div className="grw-sidebar-content-header p-3">
-      <h3 className="mb-0">{t('Bookmarks')}</h3>
-    </div>
+    <>
+      <div className="grw-sidebar-content-header p-3">
+        <h3 className="mb-0">{t('Bookmarks')}</h3>
+      </div>
+      <div className="grw-bookmarks-list p-3">
+        {isEmpty ? t('No bookmarks yet') : (
+          <ul className="list-group list-group-flush">
+            <InfiniteScroll
+              swrInifiniteResponse={swr}
+              isReachingEnd={isReachingEnd}
+            >
+              {paginationResult => paginationResult?.docs.map(data => (
+                <BookmarksItem key={data._id} data={data} />
+              ))
+              }
+            </InfiniteScroll>
+          </ul>
+        )}
+      </div>
+    </>
   );
 
 };

+ 2 - 0
packages/app/src/server/routes/apiv3/bookmarks.js

@@ -201,6 +201,7 @@ module.exports = (crowi) => {
     const { userId } = req.params;
     const page = req.query.page;
     const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM') || 30;
+    const offset = page > 0 ? (page - 1) * limit : page;
 
     if (userId == null) {
       return res.apiv3Err('User id is not found or forbidden', 400);
@@ -222,6 +223,7 @@ module.exports = (crowi) => {
               model: 'User',
             },
           },
+          offset,
           page,
           limit,
         },

+ 15 - 0
packages/app/src/stores/bookmark.ts

@@ -1,5 +1,6 @@
 import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
+import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite';
 
 import { apiv3Get } from '../client/util/apiv3-client';
 import { IBookmarkInfo } from '../interfaces/bookmark-info';
@@ -17,3 +18,17 @@ export const useSWRBookmarkInfo = (pageId: string | null | undefined): SWRRespon
     }),
   );
 };
+
+export const useSWRInifiniteBookmarkedPage = (userId: string | null | undefined) : SWRInfiniteResponse => {
+  const getKey = (page: number) => {
+    return userId != null ? `/bookmarks/${userId}/?page=${page + 1}` : null;
+  };
+  return useSWRInfinite(
+    getKey,
+    (endpoint: string) => apiv3Get<{ paginationResult }>(endpoint).then(response => response.data?.paginationResult),
+    {
+      revalidateFirstPage: false,
+      revalidateAll: false,
+    },
+  );
+};

+ 3 - 3
packages/app/src/stores/context.tsx

@@ -7,7 +7,7 @@ import { SupportedActionType } from '~/interfaces/activity';
 import { GrowiRendererConfig } from '~/interfaces/services/renderer';
 
 import { TargetAndAncestors } from '../interfaces/page-listing-results';
-import { IUser } from '../interfaces/user';
+import { IUser, IUserHasId } from '../interfaces/user';
 
 import { useStaticSWR } from './use-static-swr';
 
@@ -23,8 +23,8 @@ export const useSiteUrl = (initialData?: string): SWRResponse<string, Error> =>
   return useStaticSWR<string, Error>('siteUrl', initialData);
 };
 
-export const useCurrentUser = (initialData?: Nullable<IUser>): SWRResponse<Nullable<IUser>, Error> => {
-  return useStaticSWR<Nullable<IUser>, Error>('currentUser', initialData);
+export const useCurrentUser = (initialData?: Nullable<IUserHasId>): SWRResponse<Nullable<IUserHasId>, Error> => {
+  return useStaticSWR<Nullable<IUserHasId>, Error>('currentUser', initialData);
 };
 
 export const useRevisionId = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {

+ 13 - 0
packages/app/src/styles/theme/_apply-colors.scss

@@ -317,6 +317,19 @@ ul.pagination {
       }
     }
   }
+
+  .grw-bookmarks-list {
+    .list-group {
+      .list-group-item {
+        background-color: transparent;
+      }
+    }
+    .list-group-flush {
+      .list-group-item {
+        border: none;
+      }
+    }
+  }
 }
 
 /*