Procházet zdrojové kódy

List user root bookmarks

https://youtrack.weseek.co.jp/issue/GW-7920
- Improve get user root bookmark static method
- Update user bookmarks list route
- Update useSWRxCurrentUserBookmarks method
- Implement useSWRxCurrentUserBookmarks to BookmarkFolderTree component
- Implement bookmark item control to bookmark/un-bookmark, rename and delete page
- Add custom class and adjust styling of root bookmark items
Mudana-Grune před 3 roky
rodič
revize
010fe4db08

+ 55 - 1
packages/app/src/components/Bookmarks/BookmarkFolderTree.tsx

@@ -1,7 +1,18 @@
 
+import { useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import { toastSuccess } from '~/client/util/toastr';
+import { IPageToDeleteWithMeta } from '~/interfaces/page';
+import { OnDeletedFunction } from '~/interfaces/ui';
+import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
+import { usePageDeleteModal } from '~/stores/modal';
+import { useSWRxCurrentPage } from '~/stores/page';
 
 import BookmarkFolderItem from './BookmarkFolderItem';
+import BookmarkItem from './BookmarkItem';
 
 import styles from './BookmarkFolderTree.module.scss';
 
@@ -11,8 +22,38 @@ type BookmarkFolderTreeProps = {
 }
 
 const BookmarkFolderTree = (props: BookmarkFolderTreeProps): JSX.Element => {
-  const { data: bookmarkFolderData } = useSWRxBookamrkFolderAndChild();
+  const { t } = useTranslation();
   const { isUserHomePage } = props;
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: bookmarkFolderData } = useSWRxBookamrkFolderAndChild();
+  const { data: userBookmarks, mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
+  const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
+
+  const { open: openDeleteModal } = usePageDeleteModal();
+
+  const onUnbookmarkHandler = useCallback(() => {
+    mutateUserBookmarks();
+    mutateBookmarkInfo();
+  }, [mutateBookmarkInfo, mutateUserBookmarks]);
+
+  const onClickDeleteBookmarkHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
+    const pageDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {
+      if (typeof pathOrPathsToDelete !== 'string') {
+        return;
+      }
+      const path = pathOrPathsToDelete;
+
+      if (isCompletely) {
+        toastSuccess(t('deleted_pages_completely', { path }));
+      }
+      else {
+        toastSuccess(t('deleted_pages', { path }));
+      }
+      mutateUserBookmarks();
+      mutateBookmarkInfo();
+    };
+    openDeleteModal([pageToDelete], { onDeleted: pageDeletedHandler });
+  }, [mutateBookmarkInfo, mutateUserBookmarks, openDeleteModal, t]);
 
   return (
     <>
@@ -29,7 +70,20 @@ const BookmarkFolderTree = (props: BookmarkFolderTreeProps): JSX.Element => {
             />
           );
         })}
+        {userBookmarks?.map(page => (
+          <div key={page._id} className="grw-foldertree-item-container grw-root-bookmarks">
+            <BookmarkItem
+              bookmarkedPage={page}
+              key={page._id}
+              onUnbookmarked={onUnbookmarkHandler}
+              onRenamed={mutateUserBookmarks}
+              onClickDeleteMenuItem={onClickDeleteBookmarkHandler}
+              parentFolder={null}
+            />
+          </div>
+        ))}
       </ul>
+
     </>
   );
 

+ 6 - 3
packages/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -12,6 +12,7 @@ import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { IPageHasId, IPageInfoAll, IPageToDeleteWithMeta } from '~/interfaces/page';
+import { useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxPageInfo } from '~/stores/page';
 
@@ -25,7 +26,7 @@ type Props = {
   onUnbookmarked: () => void,
   onRenamed: () => void,
   onClickDeleteMenuItem: (pageToDelete: IPageToDeleteWithMeta) => void,
-  parentFolder: BookmarkFolderItems
+  parentFolder: BookmarkFolderItems | null
 }
 
 const BookmarkItem = (props: Props): JSX.Element => {
@@ -37,10 +38,11 @@ const BookmarkItem = (props: Props): JSX.Element => {
   const dPagePath = new DevidedPagePath(bookmarkedPage.path, false, true);
   const { latter: pageTitle, former: formerPagePath } = dPagePath;
   const bookmarkItemId = `bookmark-item-${bookmarkedPage._id}`;
-  const [parentId, setParentId] = useState(parentFolder._id);
+  const [parentId, setParentId] = useState(parentFolder?._id);
   const { mutate: mutateParentBookmarkData } = useSWRxBookamrkFolderAndChild();
   const { mutate: mutateChildFolderData } = useSWRxBookamrkFolderAndChild(parentId);
   const { data: fetchedPageInfo, mutate: mutatePageInfo } = useSWRxPageInfo(bookmarkedPage._id);
+  const { mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
 
   useEffect(() => {
     mutatePageInfo();
@@ -115,7 +117,8 @@ const BookmarkItem = (props: Props): JSX.Element => {
     type: 'BOOKMARK',
     item: bookmarkedPage,
     end: () => {
-      setParentId(parentFolder.parent);
+      setParentId(parentFolder?.parent);
+      mutateUserBookmarks();
     },
     collect: monitor => ({
       isDragging: monitor.isDragging(),

+ 15 - 33
packages/app/src/server/models/bookmark-folder.ts

@@ -1,18 +1,15 @@
-import { useId } from 'react';
-
 import { isValidObjectId } from '@growi/core/src/utils/objectid-utils';
 import monggoose, {
   Types, Document, Model, Schema,
 } from 'mongoose';
 
 
-import { IBookmarkFolder, BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import { IBookmarkFolder, BookmarkFolderItems, MyBookmarkList } from '~/interfaces/bookmark-info';
 import { IPageHasId } from '~/interfaces/page';
 
 import loggerFactory from '../../utils/logger';
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-import bookmark from './bookmark';
 import { InvalidParentBookmarkFolderError } from './errors';
 
 
@@ -34,8 +31,7 @@ export interface BookmarkFolderModel extends Model<BookmarkFolderDocument>{
   deleteFolderAndChildren(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}>
   updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string): Promise<BookmarkFolderDocument>
   insertOrUpdateBookmarkedPage(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string): Promise<BookmarkFolderDocument>
-  findBookmarksNotInFolders(folder: BookmarkFolderDocument): Promise<Types.ObjectId[]>
-  findAllBookmarksNotInFolders(userId: Types.ObjectId| string): Promise<Types.ObjectId[]>
+  findUserRootBookmarksItem(userId: Types.ObjectId| string): Promise<MyBookmarkList>
 }
 
 const bookmarkFolderSchema = new Schema<BookmarkFolderDocument, BookmarkFolderModel>({
@@ -168,33 +164,19 @@ Promise<BookmarkFolderDocument> {
   return bookmarkFolder;
 };
 
-bookmarkFolderSchema.statics.findBookmarksNotInFolders = async function(folder: BookmarkFolderDocument): Promise<Types.ObjectId[]> {
-  const bookmarkIds = (folder.bookmarks || []).map(bookmark => bookmark._id);
-  const bookmarks = await Bookmark.find({ _id: { $nin: bookmarkIds } });
-  return bookmarks.map(bookmark => bookmark._id);
-};
-
-bookmarkFolderSchema.statics.findAllBookmarksNotInFolders = async function(userId: Types.ObjectId | string): Promise<Types.ObjectId[]> {
-  const folders = await this.find({ owner: userId }).populate('bookmarks');
-  const bookmarks: Types.ObjectId[] = [];
-  await Promise.all(folders.map(async(folder) => {
-    const bookmarks = await this.findBookmarksNotInFolders(folder);
-    if (bookmarks.length > 0) {
-      bookmarks.push(...bookmarks);
-    }
-    const childFolders = await this.find({ parent: folder }).populate('bookmarks');
-    if (childFolders.length > 0) {
-      const childResult = await this.findAllBookmarksNotInFolders(userId);
-      if (childResult.length > 0) {
-        bookmarks.push(...childResult);
-      }
-    }
-  }));
-
-  const usersBookmarks = await Bookmark.find({ user: userId });
-  // TODO : Filter bookmarks not in folders
-
-  return bookmarks;
+bookmarkFolderSchema.statics.findUserRootBookmarksItem = async function(userId: Types.ObjectId | string): Promise<MyBookmarkList> {
+  const bookmarkIdsInFolders = await this.distinct('bookmarks', { owner: userId });
+  const userRootBookmarks: MyBookmarkList = await Bookmark.find({
+    _id: { $nin: bookmarkIdsInFolders },
+  }).populate({
+    path: 'page',
+    model: 'Page',
+    populate: {
+      path: 'lastUpdateUser',
+      model: 'User',
+    },
+  });
+  return userRootBookmarks;
 };
 
 export default getOrCreateModel<BookmarkFolderDocument, BookmarkFolderModel>('BookmarkFolder', bookmarkFolderSchema);

+ 6 - 30
packages/app/src/server/routes/apiv3/bookmarks.js

@@ -3,7 +3,7 @@ import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity
 import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
-
+import BookmarkFolder from '../../models/bookmark-folder';
 
 const logger = loggerFactory('growi:routes:apiv3:bookmarks'); // eslint-disable-line no-unused-vars
 
@@ -192,47 +192,23 @@ module.exports = (crowi) => {
    */
   validator.userBookmarkList = [
     param('userId').isMongoId().withMessage('userId is required'),
-    query('page').isInt({ min: 1 }),
-    query('limit').if(value => value != null).isInt({ max: 300 }).withMessage('You should set less than 300 or not to set limit.'),
   ];
 
   router.get('/:userId', accessTokenParser, loginRequired, validator.userBookmarkList, apiV3FormValidator, async(req, res) => {
     const { userId } = req.params;
-    const page = req.query.page;
-    const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM') || 30;
 
     if (userId == null) {
       return res.apiv3Err('User id is not found or forbidden', 400);
     }
-    if (limit == null) {
-      return res.apiv3Err('Could not catch page limit', 400);
-    }
     try {
-      const paginationResult = await Bookmark.paginate(
-        {
-          user: { $in: userId },
-        },
-        {
-          populate: {
-            path: 'page',
-            model: 'Page',
-            populate: {
-              path: 'lastUpdateUser',
-              model: 'User',
-            },
-          },
-          page,
-          limit,
-        },
-      );
-
-      paginationResult.docs.forEach((doc) => {
-        if (doc.page.lastUpdateUser != null && doc.page.lastUpdateUser instanceof User) {
-          doc.page.lastUpdateUser = serializeUserSecurely(doc.page.lastUpdateUser);
+      const userRootBookmarks = await BookmarkFolder.findUserRootBookmarksItem(userId);
+      userRootBookmarks.forEach((bookmark) => {
+        if (bookmark.page.lastUpdateUser != null && bookmark.page.lastUpdateUser instanceof User) {
+          bookmark.page.lastUpdateUser = serializeUserSecurely(bookmark.page.lastUpdateUser);
         }
       });
 
-      return res.apiv3({ paginationResult });
+      return res.apiv3({ userRootBookmarks });
     }
     catch (err) {
       logger.error('get-bookmark-failed', err);

+ 4 - 5
packages/app/src/stores/bookmark.ts

@@ -22,15 +22,14 @@ export const useSWRBookmarkInfo = (pageId: string | null | undefined): SWRRespon
   );
 };
 
-export const useSWRxCurrentUserBookmarks = (pageNum?: Nullable<number>): SWRResponse<IPageHasId[], Error> => {
+export const useSWRxCurrentUserBookmarks = (): SWRResponse<IPageHasId[], Error> => {
   const { data: currentUser } = useCurrentUser();
-  const currentPage = pageNum ?? 1;
   const user = currentUser as IUserHasId;
   return useSWRImmutable(
     currentUser != null ? `/bookmarks/${user._id}` : null,
-    endpoint => apiv3Get(endpoint, { page: currentPage }).then((response) => {
-      const { paginationResult } = response.data;
-      return paginationResult.docs.map((item) => {
+    endpoint => apiv3Get(endpoint).then((response) => {
+      const { userRootBookmarks } = response.data;
+      return userRootBookmarks.map((item) => {
         return {
           ...item.page,
         };

+ 4 - 0
packages/app/src/styles/molecules/_bookmark-folder-tree.scss

@@ -72,6 +72,10 @@ $grw-bookmark-item-padding-left: 45px;
     > .grw-foldertree-item-container {
       > .list-group-item {
         padding-left: 0;
+      } &.grw-root-bookmarks{
+        .list-group-item.bookmark-item-list {
+          padding-left: $grw-foldertree-item-padding-left + 25;
+        }
       }
       > .list-group-item.bookmark-item-list {
         padding-left: $grw-bookmark-item-padding-left;