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

Merge pull request #7690 from weseek/dev/6.1.x

Release v6.1.1
Yuki Takei 2 лет назад
Родитель
Сommit
496a86daf4

+ 4 - 2
apps/app/src/client/util/bookmark-utils.ts

@@ -41,6 +41,8 @@ export const toggleBookmark = async(pageId: string, status: boolean): Promise<vo
 };
 };
 
 
 // Update Bookmark folder
 // Update Bookmark folder
-export const updateBookmarkFolder = async(bookmarkFolderId: string, name: string, parent: string | null): Promise<void> => {
-  await apiv3Put('/bookmark-folder', { bookmarkFolderId, name, parent });
+export const updateBookmarkFolder = async(bookmarkFolderId: string, name: string, parent: string | null, children: BookmarkFolderItems[]): Promise<void> => {
+  await apiv3Put('/bookmark-folder', {
+    bookmarkFolderId, name, parent, children,
+  });
 };
 };

+ 40 - 34
apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -26,6 +26,7 @@ type BookmarkFolderItemProps = {
   isReadOnlyUser: boolean
   isReadOnlyUser: boolean
   bookmarkFolder: BookmarkFolderItems
   bookmarkFolder: BookmarkFolderItems
   isOpen?: boolean
   isOpen?: boolean
+  isOperable: boolean,
   level: number
   level: number
   root: string
   root: string
   isUserHomePage?: boolean
   isUserHomePage?: boolean
@@ -37,7 +38,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
   const BASE_FOLDER_PADDING = 15;
   const BASE_FOLDER_PADDING = 15;
   const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const {
   const {
-    isReadOnlyUser, bookmarkFolder, isOpen: _isOpen = false, level, root, isUserHomePage,
+    isReadOnlyUser, bookmarkFolder, isOpen: _isOpen = false, isOperable, level, root, isUserHomePage,
     onClickDeleteBookmarkHandler, bookmarkFolderTreeMutation,
     onClickDeleteBookmarkHandler, bookmarkFolderTreeMutation,
   } = props;
   } = props;
 
 
@@ -64,14 +65,15 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
   // Rename for bookmark folder handler
   // Rename for bookmark folder handler
   const onPressEnterHandlerForRename = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForRename = useCallback(async(folderName: string) => {
     try {
     try {
-      await updateBookmarkFolder(folderId, folderName, parent);
+      // TODO: do not use any type
+      await updateBookmarkFolder(folderId, folderName, parent as any, children);
       bookmarkFolderTreeMutation();
       bookmarkFolderTreeMutation();
       setIsRenameAction(false);
       setIsRenameAction(false);
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [bookmarkFolderTreeMutation, folderId, parent]);
+  }, [bookmarkFolderTreeMutation, children, folderId, parent]);
 
 
   // Create new folder / subfolder handler
   // Create new folder / subfolder handler
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
@@ -98,7 +100,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     if (dragItemType === DRAG_ITEM_TYPE.FOLDER) {
     if (dragItemType === DRAG_ITEM_TYPE.FOLDER) {
       try {
       try {
         if (item.bookmarkFolder != null) {
         if (item.bookmarkFolder != null) {
-          await updateBookmarkFolder(item.bookmarkFolder._id, item.bookmarkFolder.name, bookmarkFolder._id);
+          await updateBookmarkFolder(item.bookmarkFolder._id, item.bookmarkFolder.name, bookmarkFolder._id, item.bookmarkFolder.children);
           bookmarkFolderTreeMutation();
           bookmarkFolderTreeMutation();
         }
         }
       }
       }
@@ -148,6 +150,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
           <BookmarkFolderItem
           <BookmarkFolderItem
             key={childFolder._id}
             key={childFolder._id}
             isReadOnlyUser={isReadOnlyUser}
             isReadOnlyUser={isReadOnlyUser}
+            isOperable={props.isOperable}
             bookmarkFolder={childFolder}
             bookmarkFolder={childFolder}
             level={level + 1}
             level={level + 1}
             root={root}
             root={root}
@@ -166,6 +169,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
         <BookmarkItem
         <BookmarkItem
           key={bookmark._id}
           key={bookmark._id}
           isReadOnlyUser={isReadOnlyUser}
           isReadOnlyUser={isReadOnlyUser}
+          isOperable={props.isOperable}
           bookmarkedPage={bookmark.page}
           bookmarkedPage={bookmark.page}
           level={level + 1}
           level={level + 1}
           parentFolder={bookmarkFolder}
           parentFolder={bookmarkFolder}
@@ -197,13 +201,13 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
 
 
   const onClickMoveToRootHandlerForBookmarkFolderItemControl = useCallback(async() => {
   const onClickMoveToRootHandlerForBookmarkFolderItemControl = useCallback(async() => {
     try {
     try {
-      await updateBookmarkFolder(bookmarkFolder._id, bookmarkFolder.name, null);
+      await updateBookmarkFolder(bookmarkFolder._id, bookmarkFolder.name, null, bookmarkFolder.children);
       bookmarkFolderTreeMutation();
       bookmarkFolderTreeMutation();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [bookmarkFolder._id, bookmarkFolder.name, bookmarkFolderTreeMutation]);
+  }, [bookmarkFolder._id, bookmarkFolder.children, bookmarkFolder.name, bookmarkFolderTreeMutation]);
 
 
   return (
   return (
     <div id={`grw-bookmark-folder-item-${folderId}`} className="grw-foldertree-item-container">
     <div id={`grw-bookmark-folder-item-${folderId}`} className="grw-foldertree-item-container">
@@ -211,8 +215,8 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
         key={folderId}
         key={folderId}
         type={acceptedTypes}
         type={acceptedTypes}
         item={props}
         item={props}
-        useDragMode={true}
-        useDropMode={true}
+        useDragMode={isOperable}
+        useDropMode={isOperable}
         onDropItem={itemDropHandler}
         onDropItem={itemDropHandler}
         isDropable={isDropable}
         isDropable={isDropable}
       >
       >
@@ -252,33 +256,35 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
               </div>
               </div>
             </>
             </>
           )}
           )}
-          <div className="grw-foldertree-control d-flex">
-            <BookmarkFolderItemControl
-              onClickRename={onClickRenameHandler}
-              onClickDelete={onClickDeleteHandler}
-              onClickMoveToRoot={bookmarkFolder.parent != null
-                ? onClickMoveToRootHandlerForBookmarkFolderItemControl
-                : undefined
-              }
-            >
-              <div onClick={e => e.stopPropagation()}>
-                <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
-                  <i className="icon-options fa fa-rotate-90 p-1"></i>
-                </DropdownToggle>
-              </div>
-            </BookmarkFolderItemControl>
-            {/* Maximum folder hierarchy of 2 levels */}
-            {!(bookmarkFolder.parent != null) && (
-              <button
-                id='create-bookmark-folder-button'
-                type="button"
-                className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
-                onClick={onClickPlusButton}
+          { isOperable && (
+            <div className="grw-foldertree-control d-flex">
+              <BookmarkFolderItemControl
+                onClickRename={onClickRenameHandler}
+                onClickDelete={onClickDeleteHandler}
+                onClickMoveToRoot={bookmarkFolder.parent != null
+                  ? onClickMoveToRootHandlerForBookmarkFolderItemControl
+                  : undefined
+                }
               >
               >
-                <i className="icon-plus d-block p-0" />
-              </button>
-            )}
-          </div>
+                <div onClick={e => e.stopPropagation()}>
+                  <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
+                    <i className="icon-options fa fa-rotate-90 p-1"></i>
+                  </DropdownToggle>
+                </div>
+              </BookmarkFolderItemControl>
+              {/* Maximum folder hierarchy of 2 levels */}
+              {!(bookmarkFolder.parent != null) && (
+                <button
+                  id='create-bookmark-folder-button'
+                  type="button"
+                  className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
+                  onClick={onClickPlusButton}
+                >
+                  <i className="icon-plus d-block p-0" />
+                </button>
+              )}
+            </div>
+          )}
         </li>
         </li>
       </DragAndDropWrapper>
       </DragAndDropWrapper>
       {isCreateAction && (
       {isCreateAction && (

+ 7 - 7
apps/app/src/components/Bookmarks/BookmarkFolderMenu.tsx

@@ -6,8 +6,9 @@ import { DropdownItem, DropdownMenu, UncontrolledDropdown } from 'reactstrap';
 
 
 import { addBookmarkToFolder, toggleBookmark } from '~/client/util/bookmark-utils';
 import { addBookmarkToFolder, toggleBookmark } from '~/client/util/bookmark-utils';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
-import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
+import { useSWRBookmarkInfo, useSWRxUserBookmarks } from '~/stores/bookmark';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
+import { useCurrentUser } from '~/stores/context';
 import { useSWRxCurrentPage, useSWRxPageInfo } from '~/stores/page';
 import { useSWRxCurrentPage, useSWRxPageInfo } from '~/stores/page';
 
 
 import { BookmarkFolderMenuItem } from './BookmarkFolderMenuItem';
 import { BookmarkFolderMenuItem } from './BookmarkFolderMenuItem';
@@ -18,10 +19,12 @@ export const BookmarkFolderMenu: React.FC<{children?: React.ReactNode}> = ({ chi
   const [selectedItem, setSelectedItem] = useState<string | null>(null);
   const [selectedItem, setSelectedItem] = useState<string | null>(null);
   const [isOpen, setIsOpen] = useState(false);
   const [isOpen, setIsOpen] = useState(false);
 
 
-  const { data: bookmarkFolders, mutate: mutateBookmarkFolders } = useSWRxBookmarkFolderAndChild();
+  const { data: currentUser } = useCurrentUser();
+  const { data: bookmarkFolders, mutate: mutateBookmarkFolders } = useSWRxBookmarkFolderAndChild(currentUser?._id);
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: bookmarkInfo, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
   const { data: bookmarkInfo, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
-  const { mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
+
+  const { mutate: mutateUserBookmarks } = useSWRxUserBookmarks(currentUser?._id);
   const { mutate: mutatePageInfo } = useSWRxPageInfo(currentPage?._id);
   const { mutate: mutatePageInfo } = useSWRxPageInfo(currentPage?._id);
 
 
   const isBookmarked = bookmarkInfo?.isBookmarked ?? false;
   const isBookmarked = bookmarkInfo?.isBookmarked ?? false;
@@ -88,9 +91,6 @@ export const BookmarkFolderMenu: React.FC<{children?: React.ReactNode}> = ({ chi
     setSelectedItem(itemId);
     setSelectedItem(itemId);
 
 
     try {
     try {
-      if (isBookmarked) {
-        await toggleBookmarkHandler();
-      }
       if (currentPage != null) {
       if (currentPage != null) {
         await addBookmarkToFolder(currentPage._id, itemId === 'root' ? null : itemId);
         await addBookmarkToFolder(currentPage._id, itemId === 'root' ? null : itemId);
       }
       }
@@ -101,7 +101,7 @@ export const BookmarkFolderMenu: React.FC<{children?: React.ReactNode}> = ({ chi
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [mutateBookmarkFolders, isBookmarked, currentPage, mutateBookmarkInfo, mutateUserBookmarks, toggleBookmarkHandler]);
+  }, [mutateBookmarkFolders, currentPage, mutateBookmarkInfo, mutateUserBookmarks]);
 
 
   const renderBookmarkMenuItem = () => {
   const renderBookmarkMenuItem = () => {
     return (
     return (

+ 14 - 4
apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx

@@ -6,7 +6,7 @@ import { useTranslation } from 'next-i18next';
 import { toastSuccess } from '~/client/util/toastr';
 import { toastSuccess } from '~/client/util/toastr';
 import { IPageToDeleteWithMeta } from '~/interfaces/page';
 import { IPageToDeleteWithMeta } from '~/interfaces/page';
 import { OnDeletedFunction } from '~/interfaces/ui';
 import { OnDeletedFunction } from '~/interfaces/ui';
-import { useSWRxCurrentUserBookmarks, useSWRBookmarkInfo } from '~/stores/bookmark';
+import { useSWRxUserBookmarks, useSWRBookmarkInfo } from '~/stores/bookmark';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
 import { useIsReadOnlyUser } from '~/stores/context';
 import { useIsReadOnlyUser } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
 import { usePageDeleteModal } from '~/stores/modal';
@@ -23,15 +23,23 @@ import styles from './BookmarkFolderTree.module.scss';
 //   parentFolder: BookmarkFolderItems | null
 //   parentFolder: BookmarkFolderItems | null
 //  } & IPageHasId
 //  } & IPageHasId
 
 
-export const BookmarkFolderTree: React.FC<{isUserHomePage?: boolean}> = ({ isUserHomePage }) => {
+type Props = {
+  isUserHomePage?: boolean,
+  userId?: string,
+  isOperable: boolean,
+}
+
+export const BookmarkFolderTree: React.FC<Props> = (props: Props) => {
+  const { isUserHomePage, userId } = props;
+
   // const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   // const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
   const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
   const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
-  const { data: bookmarkFolders, mutate: mutateBookmarkFolders } = useSWRxBookmarkFolderAndChild();
-  const { data: userBookmarks, mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
+  const { data: bookmarkFolders, mutate: mutateBookmarkFolders } = useSWRxBookmarkFolderAndChild(userId);
+  const { data: userBookmarks, mutate: mutateUserBookmarks } = useSWRxUserBookmarks(userId);
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
 
 
   const bookmarkFolderTreeMutation = useCallback(() => {
   const bookmarkFolderTreeMutation = useCallback(() => {
@@ -93,6 +101,7 @@ export const BookmarkFolderTree: React.FC<{isUserHomePage?: boolean}> = ({ isUse
             <BookmarkFolderItem
             <BookmarkFolderItem
               key={bookmarkFolder._id}
               key={bookmarkFolder._id}
               isReadOnlyUser={!!isReadOnlyUser}
               isReadOnlyUser={!!isReadOnlyUser}
+              isOperable={props.isOperable}
               bookmarkFolder={bookmarkFolder}
               bookmarkFolder={bookmarkFolder}
               isOpen={false}
               isOpen={false}
               level={0}
               level={0}
@@ -108,6 +117,7 @@ export const BookmarkFolderTree: React.FC<{isUserHomePage?: boolean}> = ({ isUse
             <BookmarkItem
             <BookmarkItem
               key={userBookmark._id}
               key={userBookmark._id}
               isReadOnlyUser={!!isReadOnlyUser}
               isReadOnlyUser={!!isReadOnlyUser}
+              isOperable={props.isOperable}
               bookmarkedPage={userBookmark}
               bookmarkedPage={userBookmark}
               level={0}
               level={0}
               parentFolder={null}
               parentFolder={null}

+ 5 - 2
apps/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -23,6 +23,7 @@ import { DragAndDropWrapper } from './DragAndDropWrapper';
 
 
 type Props = {
 type Props = {
   isReadOnlyUser: boolean
   isReadOnlyUser: boolean
+  isOperable: boolean,
   bookmarkedPage: IPageHasId,
   bookmarkedPage: IPageHasId,
   level: number,
   level: number,
   parentFolder: BookmarkFolderItems | null,
   parentFolder: BookmarkFolderItems | null,
@@ -38,7 +39,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const {
   const {
-    isReadOnlyUser, bookmarkedPage, onClickDeleteBookmarkHandler,
+    isReadOnlyUser, isOperable, bookmarkedPage, onClickDeleteBookmarkHandler,
     parentFolder, level, canMoveToRoot, bookmarkFolderTreeMutation,
     parentFolder, level, canMoveToRoot, bookmarkFolderTreeMutation,
   } = props;
   } = props;
 
 
@@ -113,7 +114,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
     <DragAndDropWrapper
     <DragAndDropWrapper
       item={dragItem}
       item={dragItem}
       type={[DRAG_ITEM_TYPE.BOOKMARK]}
       type={[DRAG_ITEM_TYPE.BOOKMARK]}
-      useDragMode={true}
+      useDragMode={isOperable}
     >
     >
       <li
       <li
         className="grw-bookmark-item-list list-group-item list-group-item-action border-0 py-0 mr-auto d-flex align-items-center"
         className="grw-bookmark-item-list list-group-item list-group-item-action border-0 py-0 mr-auto d-flex align-items-center"
@@ -130,6 +131,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
             validationTarget={ValidationTarget.PAGE}
             validationTarget={ValidationTarget.PAGE}
           />
           />
         ) : <PageListItemS page={bookmarkedPage} pageTitle={pageTitle}/>}
         ) : <PageListItemS page={bookmarkedPage} pageTitle={pageTitle}/>}
+
         <div className='grw-foldertree-control'>
         <div className='grw-foldertree-control'>
           <PageItemControl
           <PageItemControl
             pageId={bookmarkedPage._id}
             pageId={bookmarkedPage._id}
@@ -149,6 +151,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
             </DropdownToggle>
             </DropdownToggle>
           </PageItemControl>
           </PageItemControl>
         </div>
         </div>
+
         <UncontrolledTooltip
         <UncontrolledTooltip
           modifiers={{ preventOverflow: { boundariesElement: 'window' } }}
           modifiers={{ preventOverflow: { boundariesElement: 'window' } }}
           autohide={false}
           autohide={false}

+ 3 - 3
apps/app/src/components/PageList/PageListItemL.tsx

@@ -24,7 +24,7 @@ import {
   OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
   OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
 } from '~/interfaces/ui';
 } from '~/interfaces/ui';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
-import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
+import { useSWRBookmarkInfo, useSWRxUserBookmarks } from '~/stores/bookmark';
 import {
 import {
   usePageRenameModal, usePageDuplicateModal, usePageDeleteModal, usePutBackPageModal,
   usePageRenameModal, usePageDuplicateModal, usePageDeleteModal, usePutBackPageModal,
 } from '~/stores/modal';
 } from '~/stores/modal';
@@ -90,7 +90,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
 
 
   const shouldFetch = isSelected && (pageData != null || pageMeta != null);
   const shouldFetch = isSelected && (pageData != null || pageMeta != null);
   const { data: pageInfo } = useSWRxPageInfo(shouldFetch ? pageData?._id : null);
   const { data: pageInfo } = useSWRxPageInfo(shouldFetch ? pageData?._id : null);
-  const { mutate: mutateCurrentUserBookmark } = useSWRxCurrentUserBookmarks();
+  const { mutate: mutateUserBookmark } = useSWRxUserBookmarks();
   const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(pageData?._id);
   const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(pageData?._id);
   const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null;
   const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null;
   const revisionShortBody = isIPageInfoForListing(pageMeta) ? pageMeta.revisionShortBody : null;
   const revisionShortBody = isIPageInfoForListing(pageMeta) ? pageMeta.revisionShortBody : null;
@@ -128,7 +128,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
   const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
   const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
     const bookmarkOperation = _newValue ? bookmark : unbookmark;
     const bookmarkOperation = _newValue ? bookmark : unbookmark;
     await bookmarkOperation(_pageId);
     await bookmarkOperation(_pageId);
-    mutateCurrentUserBookmark();
+    mutateUserBookmark();
     mutateBookmarkInfo();
     mutateBookmarkInfo();
   };
   };
 
 

+ 2 - 2
apps/app/src/components/PageRenameModal.tsx

@@ -255,7 +255,7 @@ const PageRenameModal = (): JSX.Element => {
             <div className="custom-control custom-radio custom-radio-warning">
             <div className="custom-control custom-radio custom-radio-warning">
               <input
               <input
                 className="custom-control-input"
                 className="custom-control-input"
-                name="recursively"
+                name="withoutExistRecursively"
                 id="cbRenameThisPageOnly"
                 id="cbRenameThisPageOnly"
                 type="radio"
                 type="radio"
                 checked={!isRenameRecursively}
                 checked={!isRenameRecursively}
@@ -268,7 +268,7 @@ const PageRenameModal = (): JSX.Element => {
             <div className="custom-control custom-radio custom-radio-warning mt-1">
             <div className="custom-control custom-radio custom-radio-warning mt-1">
               <input
               <input
                 className="custom-control-input"
                 className="custom-control-input"
-                name="withoutExistRecursively"
+                name="recursively"
                 id="cbForceRenameRecursively"
                 id="cbForceRenameRecursively"
                 type="radio"
                 type="radio"
                 checked={isRenameRecursively}
                 checked={isRenameRecursively}

+ 5 - 2
apps/app/src/components/Sidebar/Bookmarks/BookmarkContents.tsx

@@ -8,12 +8,15 @@ import { BookmarkFolderNameInput } from '~/components/Bookmarks/BookmarkFolderNa
 import { BookmarkFolderTree } from '~/components/Bookmarks/BookmarkFolderTree';
 import { BookmarkFolderTree } from '~/components/Bookmarks/BookmarkFolderTree';
 import { FolderPlusIcon } from '~/components/Icons/FolderPlusIcon';
 import { FolderPlusIcon } from '~/components/Icons/FolderPlusIcon';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
+import { useCurrentUser } from '~/stores/context';
 
 
 export const BookmarkContents = (): JSX.Element => {
 export const BookmarkContents = (): JSX.Element => {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
   const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
-  const { mutate: mutateBookmarkFolders } = useSWRxBookmarkFolderAndChild();
+
+  const { data: currentUser } = useCurrentUser();
+  const { mutate: mutateBookmarkFolders } = useSWRxBookmarkFolderAndChild(currentUser?._id);
 
 
   const onClickNewBookmarkFolder = useCallback(() => {
   const onClickNewBookmarkFolder = useCallback(() => {
     setIsCreateAction(true);
     setIsCreateAction(true);
@@ -53,7 +56,7 @@ export const BookmarkContents = (): JSX.Element => {
           />
           />
         </div>
         </div>
       )}
       )}
-      <BookmarkFolderTree />
+      <BookmarkFolderTree isOperable userId={currentUser?._id} />
     </>
     </>
   );
   );
 };
 };

+ 3 - 3
apps/app/src/components/Sidebar/PageTree/Item.tsx

@@ -22,7 +22,7 @@ import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnl
 import {
 import {
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
-import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
+import { useSWRBookmarkInfo, useSWRxUserBookmarks } from '~/stores/bookmark';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
 import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import { usePageTreeDescCountMap } from '~/stores/ui';
@@ -124,7 +124,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const [isCreating, setCreating] = useState(false);
   const [isCreating, setCreating] = useState(false);
 
 
   const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
   const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
-  const { mutate: mutateCurrentUserBookmarks } = useSWRxCurrentUserBookmarks();
+  const { mutate: mutateUserBookmarks } = useSWRxUserBookmarks();
   const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(page._id);
   const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(page._id);
 
 
   // descendantCount
   // descendantCount
@@ -261,7 +261,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
   const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
     const bookmarkOperation = _newValue ? bookmark : unbookmark;
     const bookmarkOperation = _newValue ? bookmark : unbookmark;
     await bookmarkOperation(_pageId);
     await bookmarkOperation(_pageId);
-    mutateCurrentUserBookmarks();
+    mutateUserBookmarks();
     mutateBookmarkInfo();
     mutateBookmarkInfo();
   };
   };
 
 

+ 5 - 1
apps/app/src/components/UsersHomePageFooter.tsx

@@ -2,9 +2,11 @@ import React, { useState } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
+
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 import { RecentCreated } from '~/components/RecentCreated/RecentCreated';
 import { RecentCreated } from '~/components/RecentCreated/RecentCreated';
 import styles from '~/components/UsersHomePageFooter.module.scss';
 import styles from '~/components/UsersHomePageFooter.module.scss';
+import { useCurrentUser } from '~/stores/context';
 
 
 import { BookmarkFolderTree } from './Bookmarks/BookmarkFolderTree';
 import { BookmarkFolderTree } from './Bookmarks/BookmarkFolderTree';
 import { CompressIcon } from './Icons/CompressIcon';
 import { CompressIcon } from './Icons/CompressIcon';
@@ -18,6 +20,8 @@ export const UsersHomePageFooter = (props: UsersHomePageFooterProps): JSX.Elemen
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { creatorId } = props;
   const { creatorId } = props;
   const [isExpanded, setIsExpanded] = useState<boolean>(false);
   const [isExpanded, setIsExpanded] = useState<boolean>(false);
+  const { data: currentUser } = useCurrentUser();
+  const isOperable = currentUser?._id === creatorId;
 
 
   return (
   return (
     <div className={`container-lg user-page-footer py-5 ${styles['user-page-footer']}`}>
     <div className={`container-lg user-page-footer py-5 ${styles['user-page-footer']}`}>
@@ -39,7 +43,7 @@ export const UsersHomePageFooter = (props: UsersHomePageFooterProps): JSX.Elemen
         </h2>
         </h2>
         {/* TODO: In bookmark folders v1, the button to create a new folder does not exist. The button should be included in the bookmark component. */}
         {/* TODO: In bookmark folders v1, the button to create a new folder does not exist. The button should be included in the bookmark component. */}
         <div className={`${isExpanded ? `${styles['grw-bookarks-contents-expanded']}` : `${styles['grw-bookarks-contents-compressed']}`}`}>
         <div className={`${isExpanded ? `${styles['grw-bookarks-contents-expanded']}` : `${styles['grw-bookarks-contents-compressed']}`}`}>
-          <BookmarkFolderTree isUserHomePage={true} />
+          <BookmarkFolderTree isUserHomePage={true} isOperable={isOperable} userId={creatorId} />
         </div>
         </div>
       </div>
       </div>
       <div className="grw-user-page-list-m mt-5 d-edit-none">
       <div className="grw-user-page-list-m mt-5 d-edit-none">

+ 10 - 13
apps/app/src/interfaces/bookmark-info.ts

@@ -3,13 +3,13 @@ import { Ref } from '@growi/core';
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
 import { IUser } from '~/interfaces/user';
 import { IUser } from '~/interfaces/user';
 
 
-export type IBookmarkInfo = {
+export interface IBookmarkInfo {
   sumOfBookmarks: number;
   sumOfBookmarks: number;
   isBookmarked: boolean,
   isBookmarked: boolean,
   bookmarkedUsers: IUser[]
   bookmarkedUsers: IUser[]
-};
+}
 
 
-type BookmarkedPage = {
+export interface BookmarkedPage {
   _id: string,
   _id: string,
   page: IPageHasId,
   page: IPageHasId,
   user: Ref<IUser>,
   user: Ref<IUser>,
@@ -24,12 +24,10 @@ export interface IBookmarkFolder {
   parent?: Ref<this>
   parent?: Ref<this>
 }
 }
 
 
-export interface BookmarkFolderItems {
-  _id: string
-  name: string
-  parent: string
-  children: this[]
-  bookmarks: BookmarkedPage[]
+export interface BookmarkFolderItems extends IBookmarkFolder {
+  _id: string;
+  children: BookmarkFolderItems[];
+  bookmarks: BookmarkedPage[];
 }
 }
 
 
 export const DRAG_ITEM_TYPE = {
 export const DRAG_ITEM_TYPE = {
@@ -37,15 +35,14 @@ export const DRAG_ITEM_TYPE = {
   BOOKMARK: 'BOOKMARK',
   BOOKMARK: 'BOOKMARK',
 } as const;
 } as const;
 
 
-type BookmarkDragItem = {
+interface BookmarkDragItem {
   bookmarkFolder: BookmarkFolderItems
   bookmarkFolder: BookmarkFolderItems
   level: number
   level: number
   root: string
   root: string
 }
 }
 
 
-export type DragItemDataType = BookmarkDragItem & {
+export interface DragItemDataType extends BookmarkDragItem, IPageHasId {
   parentFolder: BookmarkFolderItems | null
   parentFolder: BookmarkFolderItems | null
-} & IPageHasId
-
+}
 
 
 export type DragItemType = typeof DRAG_ITEM_TYPE[keyof typeof DRAG_ITEM_TYPE];
 export type DragItemType = typeof DRAG_ITEM_TYPE[keyof typeof DRAG_ITEM_TYPE];

+ 22 - 72
apps/app/src/server/models/bookmark-folder.ts

@@ -3,7 +3,7 @@ import monggoose, {
   Types, Document, Model, Schema,
   Types, Document, Model, Schema,
 } from 'mongoose';
 } from 'mongoose';
 
 
-import { IBookmarkFolder, BookmarkFolderItems, MyBookmarkList } from '~/interfaces/bookmark-info';
+import { BookmarkFolderItems, IBookmarkFolder } from '~/interfaces/bookmark-info';
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
 
 
 import loggerFactory from '../../utils/logger';
 import loggerFactory from '../../utils/logger';
@@ -26,11 +26,9 @@ export interface BookmarkFolderDocument extends Document {
 
 
 export interface BookmarkFolderModel extends Model<BookmarkFolderDocument>{
 export interface BookmarkFolderModel extends Model<BookmarkFolderDocument>{
   createByParameters(params: IBookmarkFolder): Promise<BookmarkFolderDocument>
   createByParameters(params: IBookmarkFolder): Promise<BookmarkFolderDocument>
-  findFolderAndChildren(user: Types.ObjectId | string, parentId?: Types.ObjectId | string): Promise<BookmarkFolderItems[]>
   deleteFolderAndChildren(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}>
   deleteFolderAndChildren(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}>
-  updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string | null): Promise<BookmarkFolderDocument>
+  updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string | null, children: BookmarkFolderItems[]): Promise<BookmarkFolderDocument>
   insertOrUpdateBookmarkedPage(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null): Promise<BookmarkFolderDocument | null>
   insertOrUpdateBookmarkedPage(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null): Promise<BookmarkFolderDocument | null>
-  findUserRootBookmarksItem(userId: Types.ObjectId| string): Promise<MyBookmarkList>
   updateBookmark(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId| string): Promise<BookmarkFolderDocument | null>
   updateBookmark(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId| string): Promise<BookmarkFolderDocument | null>
 }
 }
 
 
@@ -83,45 +81,6 @@ bookmarkFolderSchema.statics.createByParameters = async function(params: IBookma
   return bookmarkFolder;
   return bookmarkFolder;
 };
 };
 
 
-bookmarkFolderSchema.statics.findFolderAndChildren = async function(
-    userId: Types.ObjectId | string,
-    parentId?: Types.ObjectId | string,
-): Promise<BookmarkFolderItems[]> {
-  const folderItems: BookmarkFolderItems[] = [];
-
-  const folders = await this.find({ owner: userId, parent: parentId })
-    .populate('children')
-    .populate({
-      path: 'bookmarks',
-      model: 'Bookmark',
-      populate: {
-        path: 'page',
-        model: 'Page',
-      },
-    });
-
-  const promises = folders.map(async(folder) => {
-    const children = await this.findFolderAndChildren(userId, folder._id);
-    const {
-      _id, name, owner, bookmarks, parent,
-    } = folder;
-
-    const res = {
-      _id: _id.toString(),
-      name,
-      owner,
-      bookmarks,
-      children,
-      parent,
-    };
-    return res;
-  });
-
-  const results = await Promise.all(promises) as unknown as BookmarkFolderItems[];
-  folderItems.push(...results);
-  return folderItems;
-};
-
 bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}> {
 bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}> {
   const bookmarkFolder = await this.findById(bookmarkFolderId);
   const bookmarkFolder = await this.findById(bookmarkFolderId);
   // Delete parent and all children folder
   // Delete parent and all children folder
@@ -145,7 +104,12 @@ bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFo
   return { deletedCount };
   return { deletedCount };
 };
 };
 
 
-bookmarkFolderSchema.statics.updateBookmarkFolder = async function(bookmarkFolderId: string, name: string, parentId: string | null):
+bookmarkFolderSchema.statics.updateBookmarkFolder = async function(
+    bookmarkFolderId: string,
+    name: string,
+    parentId: string | null,
+    children: BookmarkFolderItems[],
+):
  Promise<BookmarkFolderDocument> {
  Promise<BookmarkFolderDocument> {
   const updateFields: {name: string, parent: Types.ObjectId | null} = {
   const updateFields: {name: string, parent: Types.ObjectId | null} = {
     name: '',
     name: '',
@@ -163,8 +127,7 @@ bookmarkFolderSchema.statics.updateBookmarkFolder = async function(bookmarkFolde
     if (parentFolder?.parent != null) {
     if (parentFolder?.parent != null) {
       throw new Error('Update bookmark folder failed');
       throw new Error('Update bookmark folder failed');
     }
     }
-    const bookmarkFolder = await this.findById(bookmarkFolderId).populate('children');
-    if (bookmarkFolder?.children?.length !== 0) {
+    if (children.length !== 0) {
       throw new Error('Update bookmark folder failed');
       throw new Error('Update bookmark folder failed');
     }
     }
   }
   }
@@ -179,43 +142,30 @@ bookmarkFolderSchema.statics.updateBookmarkFolder = async function(bookmarkFolde
 
 
 bookmarkFolderSchema.statics.insertOrUpdateBookmarkedPage = async function(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null):
 bookmarkFolderSchema.statics.insertOrUpdateBookmarkedPage = async function(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null):
 Promise<BookmarkFolderDocument | null> {
 Promise<BookmarkFolderDocument | null> {
-
-  // Create bookmark or update existing
-  const bookmarkedPage = await Bookmark.findOneAndUpdate({ page: pageId, user: userId }, { page: pageId, user: userId }, { new: true, upsert: true });
+  // Find bookmark
+  const bookmarkedPage = await Bookmark.findOne({ page: pageId, user: userId }, { new: true, upsert: true });
 
 
   // Remove existing bookmark in bookmark folder
   // Remove existing bookmark in bookmark folder
-  await this.updateMany({}, { $pull: { bookmarks:  bookmarkedPage._id } });
-
-  // Insert bookmark into bookmark folder
-  if (folderId != null) {
-    const bookmarkFolder = await this.findByIdAndUpdate(folderId, { $addToSet: { bookmarks: bookmarkedPage } }, { new: true, upsert: true });
-    return bookmarkFolder;
+  await this.updateMany({ owner: userId }, { $pull: { bookmarks:  bookmarkedPage?._id } });
+  if (folderId == null) {
+    return null;
   }
   }
 
 
-  return null;
-};
+  // Insert bookmark into bookmark folder
+  const bookmarkFolder = await this.findByIdAndUpdate(
+    { _id: folderId, owner: userId },
+    { $addToSet: { bookmarks: bookmarkedPage } },
+    { new: true, upsert: true },
+  );
 
 
-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 },
-    user: userId,
-  }).populate({
-    path: 'page',
-    model: 'Page',
-    populate: {
-      path: 'lastUpdateUser',
-      model: 'User',
-    },
-  });
-  return userRootBookmarks;
+  return bookmarkFolder;
 };
 };
 
 
 bookmarkFolderSchema.statics.updateBookmark = async function(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId | string):
 bookmarkFolderSchema.statics.updateBookmark = async function(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId | string):
 Promise<BookmarkFolderDocument | null> {
 Promise<BookmarkFolderDocument | null> {
   // If isBookmarked
   // If isBookmarked
   if (status) {
   if (status) {
-    const bookmarkedPage = await Bookmark.findOne({ page: pageId });
+    const bookmarkedPage = await Bookmark.findOne({ page: pageId, user: userId });
     const bookmarkFolder = await this.findOne({ owner: userId, bookmarks: { $in: [bookmarkedPage?._id] } });
     const bookmarkFolder = await this.findOne({ owner: userId, bookmarks: { $in: [bookmarkedPage?._id] } });
     if (bookmarkFolder != null) {
     if (bookmarkFolder != null) {
       await this.updateOne({ owner: userId, _id: bookmarkFolder._id }, { $pull: { bookmarks:  bookmarkedPage?._id } });
       await this.updateOne({ owner: userId, _id: bookmarkFolder._id }, { $pull: { bookmarks:  bookmarkedPage?._id } });

+ 25 - 0
apps/app/src/server/models/serializers/bookmark-serializer.js

@@ -0,0 +1,25 @@
+const { serializePageSecurely } = require('./page-serializer');
+
+function serializeInsecurePageAttributes(bookmark) {
+  if (bookmark.page != null && bookmark.page._id != null) {
+    bookmark.page = serializePageSecurely(bookmark.page);
+  }
+  return bookmark;
+}
+
+function serializeBookmarkSecurely(bookmark) {
+  let serialized = bookmark;
+
+  // invoke toObject if bookmark is a model instance
+  if (bookmark.toObject != null) {
+    serialized = bookmark.toObject();
+  }
+
+  serializeInsecurePageAttributes(serialized);
+
+  return serialized;
+}
+
+module.exports = {
+  serializeBookmarkSecurely,
+};

+ 61 - 5
apps/app/src/server/routes/apiv3/bookmark-folder.ts

@@ -1,14 +1,16 @@
 import { ErrorV3 } from '@growi/core';
 import { ErrorV3 } from '@growi/core';
 import { body } from 'express-validator';
 import { body } from 'express-validator';
+import { Types } from 'mongoose';
 
 
+import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { InvalidParentBookmarkFolderError } from '~/server/models/errors';
 import { InvalidParentBookmarkFolderError } from '~/server/models/errors';
+import { serializeBookmarkSecurely } from '~/server/models/serializers/bookmark-serializer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import BookmarkFolder from '../../models/bookmark-folder';
 import BookmarkFolder from '../../models/bookmark-folder';
 
 
 const logger = loggerFactory('growi:routes:apiv3:bookmark-folder');
 const logger = loggerFactory('growi:routes:apiv3:bookmark-folder');
-
 const express = require('express');
 const express = require('express');
 
 
 const router = express.Router();
 const router = express.Router();
@@ -23,6 +25,8 @@ const validator = {
           throw new Error('Maximum folder hierarchy of 2 levels');
           throw new Error('Maximum folder hierarchy of 2 levels');
         }
         }
       }),
       }),
+    body('children').optional().isArray().withMessage('Children must be an array'),
+    body('bookmarkFolderId').optional().isMongoId().withMessage('Bookark Folder ID must be a valid mongo ID'),
   ],
   ],
   bookmarkPage: [
   bookmarkPage: [
     body('pageId').isMongoId().withMessage('Page ID must be a valid mongo ID'),
     body('pageId').isMongoId().withMessage('Page ID must be a valid mongo ID'),
@@ -52,6 +56,7 @@ module.exports = (crowi) => {
       return res.apiv3({ bookmarkFolder });
       return res.apiv3({ bookmarkFolder });
     }
     }
     catch (err) {
     catch (err) {
+      logger.error(err);
       if (err instanceof InvalidParentBookmarkFolderError) {
       if (err instanceof InvalidParentBookmarkFolderError) {
         return res.apiv3Err(new ErrorV3(err.message, 'failed_to_create_bookmark_folder'));
         return res.apiv3Err(new ErrorV3(err.message, 'failed_to_create_bookmark_folder'));
       }
       }
@@ -60,14 +65,60 @@ module.exports = (crowi) => {
   });
   });
 
 
   // List bookmark folders and child
   // List bookmark folders and child
-  router.get('/list', accessTokenParser, loginRequiredStrictly, async(req, res) => {
+  router.get('/list/:userId', accessTokenParser, loginRequiredStrictly, async(req, res) => {
+    const { userId } = req.params;
+
+    const getBookmarkFolders = async(
+        userId: Types.ObjectId | string,
+        parentFolderId?: Types.ObjectId | string,
+    ) => {
+      const folders = await BookmarkFolder.find({ owner: userId, parent: parentFolderId })
+        .populate('children')
+        .populate({
+          path: 'bookmarks',
+          model: 'Bookmark',
+          populate: {
+            path: 'page',
+            model: 'Page',
+            populate: {
+              path: 'lastUpdateUser',
+              model: 'User',
+            },
+          },
+        }).exec();
+
+      const returnValue: BookmarkFolderItems[] = [];
+
+      const promises = folders.map(async(folder: BookmarkFolderItems) => {
+        const children = await getBookmarkFolders(userId, folder._id);
+
+        // !! DO NOT THIS SERIALIZING OUTSIDE OF PROMISES !! -- 05.23.2023 ryoji-s
+        // Serializing outside of promises will cause not populated.
+        const bookmarks = folder.bookmarks.map(bookmark => serializeBookmarkSecurely(bookmark));
+
+        const res = {
+          _id: folder._id.toString(),
+          name: folder.name,
+          owner: folder.owner,
+          bookmarks,
+          children,
+          parent: folder.parent,
+        };
+        return res;
+      });
+
+      const results = await Promise.all(promises) as unknown as BookmarkFolderItems[];
+      returnValue.push(...results);
+      return returnValue;
+    };
 
 
     try {
     try {
-      const bookmarkFolderItems = await BookmarkFolder.findFolderAndChildren(req.user?._id);
+      const bookmarkFolderItems = await getBookmarkFolders(userId, undefined);
 
 
       return res.apiv3({ bookmarkFolderItems });
       return res.apiv3({ bookmarkFolderItems });
     }
     }
     catch (err) {
     catch (err) {
+      logger.error(err);
       return res.apiv3Err(err, 500);
       return res.apiv3Err(err, 500);
     }
     }
   });
   });
@@ -87,12 +138,15 @@ module.exports = (crowi) => {
   });
   });
 
 
   router.put('/', accessTokenParser, loginRequiredStrictly, validator.bookmarkFolder, async(req, res) => {
   router.put('/', accessTokenParser, loginRequiredStrictly, validator.bookmarkFolder, async(req, res) => {
-    const { bookmarkFolderId, name, parent } = req.body;
+    const {
+      bookmarkFolderId, name, parent, children,
+    } = req.body;
     try {
     try {
-      const bookmarkFolder = await BookmarkFolder.updateBookmarkFolder(bookmarkFolderId, name, parent);
+      const bookmarkFolder = await BookmarkFolder.updateBookmarkFolder(bookmarkFolderId, name, parent, children);
       return res.apiv3({ bookmarkFolder });
       return res.apiv3({ bookmarkFolder });
     }
     }
     catch (err) {
     catch (err) {
+      logger.error(err);
       return res.apiv3Err(err, 500);
       return res.apiv3Err(err, 500);
     }
     }
   });
   });
@@ -107,6 +161,7 @@ module.exports = (crowi) => {
       return res.apiv3({ bookmarkFolder });
       return res.apiv3({ bookmarkFolder });
     }
     }
     catch (err) {
     catch (err) {
+      logger.error(err);
       return res.apiv3Err(err, 500);
       return res.apiv3Err(err, 500);
     }
     }
   });
   });
@@ -119,6 +174,7 @@ module.exports = (crowi) => {
       return res.apiv3({ bookmarkFolder });
       return res.apiv3({ bookmarkFolder });
     }
     }
     catch (err) {
     catch (err) {
+      logger.error(err);
       return res.apiv3Err(err, 500);
       return res.apiv3Err(err, 500);
     }
     }
   });
   });

+ 17 - 7
apps/app/src/server/routes/apiv3/bookmarks.js

@@ -1,5 +1,6 @@
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
+import { serializeBookmarkSecurely } from '~/server/models/serializers/bookmark-serializer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
@@ -201,14 +202,23 @@ module.exports = (crowi) => {
       return res.apiv3Err('User id is not found or forbidden', 400);
       return res.apiv3Err('User id is not found or forbidden', 400);
     }
     }
     try {
     try {
-      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);
-        }
-      });
+      const bookmarkIdsInFolders = await BookmarkFolder.distinct('bookmarks', { owner: userId });
+      const userRootBookmarks = await Bookmark.find({
+        _id: { $nin: bookmarkIdsInFolders },
+        user: userId,
+      }).populate({
+        path: 'page',
+        model: 'Page',
+        populate: {
+          path: 'lastUpdateUser',
+          model: 'User',
+        },
+      }).exec();
+
+      // serialize Bookmark
+      const serializedUserRootBookmarks = userRootBookmarks.map(bookmark => serializeBookmarkSecurely(bookmark));
 
 
-      return res.apiv3({ userRootBookmarks });
+      return res.apiv3({ userRootBookmarks: serializedUserRootBookmarks });
     }
     }
     catch (err) {
     catch (err) {
       logger.error('get-bookmark-failed', err);
       logger.error('get-bookmark-failed', err);

+ 2 - 2
apps/app/src/stores/bookmark-folder.ts

@@ -4,10 +4,10 @@ import useSWRImmutable from 'swr/immutable';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 
 
-export const useSWRxBookmarkFolderAndChild = (): SWRResponse<BookmarkFolderItems[], Error> => {
+export const useSWRxBookmarkFolderAndChild = (userId?: string): SWRResponse<BookmarkFolderItems[], Error> => {
 
 
   return useSWRImmutable(
   return useSWRImmutable(
-    '/bookmark-folder/list',
+    userId != null ? `/bookmark-folder/list/${userId}` : null,
     endpoint => apiv3Get(endpoint).then((response) => {
     endpoint => apiv3Get(endpoint).then((response) => {
       return response.data.bookmarkFolderItems;
       return response.data.bookmarkFolderItems;
     }),
     }),

+ 2 - 7
apps/app/src/stores/bookmark.ts

@@ -1,4 +1,3 @@
-import { IUserHasId } from '@growi/core';
 import { SWRResponse } from 'swr';
 import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
@@ -7,8 +6,6 @@ import { IPageHasId } from '~/interfaces/page';
 import { apiv3Get } from '../client/util/apiv3-client';
 import { apiv3Get } from '../client/util/apiv3-client';
 import { IBookmarkInfo } from '../interfaces/bookmark-info';
 import { IBookmarkInfo } from '../interfaces/bookmark-info';
 
 
-import { useCurrentUser } from './context';
-
 export const useSWRBookmarkInfo = (pageId: string | null | undefined): SWRResponse<IBookmarkInfo, Error> => {
 export const useSWRBookmarkInfo = (pageId: string | null | undefined): SWRResponse<IBookmarkInfo, Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
     pageId != null ? `/bookmarks/info?pageId=${pageId}` : null,
     pageId != null ? `/bookmarks/info?pageId=${pageId}` : null,
@@ -22,11 +19,9 @@ export const useSWRBookmarkInfo = (pageId: string | null | undefined): SWRRespon
   );
   );
 };
 };
 
 
-export const useSWRxCurrentUserBookmarks = (): SWRResponse<IPageHasId[], Error> => {
-  const { data: currentUser } = useCurrentUser();
-  const user = currentUser as IUserHasId;
+export const useSWRxUserBookmarks = (userId?: string): SWRResponse<IPageHasId[], Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
-    currentUser != null ? `/bookmarks/${user._id}` : null,
+    userId != null ? `/bookmarks/${userId}` : null,
     endpoint => apiv3Get(endpoint).then((response) => {
     endpoint => apiv3Get(endpoint).then((response) => {
       const { userRootBookmarks } = response.data;
       const { userRootBookmarks } = response.data;
       return userRootBookmarks.map((item) => {
       return userRootBookmarks.map((item) => {

+ 2 - 0
packages/remark-attachment-refs/vite.client.config.ts

@@ -1,9 +1,11 @@
+import react from '@vitejs/plugin-react';
 import { defineConfig } from 'vite';
 import { defineConfig } from 'vite';
 import dts from 'vite-plugin-dts';
 import dts from 'vite-plugin-dts';
 
 
 // https://vitejs.dev/config/
 // https://vitejs.dev/config/
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
+    react(),
     dts(),
     dts(),
   ],
   ],
   build: {
   build: {