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

Merge pull request #8703 from weseek/support/143856-bookmark-sidebar-rayout

imprv: Bookmark sidebar layout
Yuki Takei 2 лет назад
Родитель
Сommit
d9b255023b

+ 8 - 12
apps/app/src/client/util/bookmark-utils.ts

@@ -1,18 +1,12 @@
 import type { IRevision, Ref } from '@growi/core';
 
-import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import type { BookmarkFolderItems, BookmarkedPage } from '~/interfaces/bookmark-info';
 
 import { apiv3Delete, apiv3Post, apiv3Put } from './apiv3-client';
 
-// Check if bookmark folder item has children
-export const hasChildren = (item: BookmarkFolderItems | BookmarkFolderItems[]): boolean => {
-  if (item === null) {
-    return false;
-  }
-  if (Array.isArray(item)) {
-    return item.length > 0;
-  }
-  return item.children && item.children.length > 0;
+// Check if bookmark folder item has childFolder or bookmarks
+export const hasChildren = ({ childFolder, bookmarks }: { childFolder?: BookmarkFolderItems[], bookmarks?: BookmarkedPage[] }): boolean => {
+  return !!((childFolder && childFolder.length > 0) || (bookmarks && bookmarks.length > 0));
 };
 
 // Add new folder helper
@@ -41,8 +35,10 @@ export const toggleBookmark = async(pageId: string, status: boolean): Promise<vo
 };
 
 // Update Bookmark folder
-export const updateBookmarkFolder = async(bookmarkFolderId: string, name: string, parent: string | null, children: BookmarkFolderItems[]): Promise<void> => {
+export const updateBookmarkFolder = async(
+    bookmarkFolderId: string, name: string, parent: string | null, childFolder: BookmarkFolderItems[],
+): Promise<void> => {
   await apiv3Put('/bookmark-folder', {
-    bookmarkFolderId, name, parent, children,
+    bookmarkFolderId, name, parent, childFolder,
   });
 };

+ 31 - 29
apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -1,6 +1,5 @@
-import {
-  FC, useCallback, useState,
-} from 'react';
+import type { FC } from 'react';
+import { useCallback, useState } from 'react';
 
 import type { IPageToDeleteWithMeta } from '@growi/core';
 import { DropdownToggle } from 'reactstrap';
@@ -10,10 +9,9 @@ import {
 } from '~/client/util/bookmark-utils';
 import { toastError } from '~/client/util/toastr';
 import { FolderIcon } from '~/components/Icons/FolderIcon';
-import {
-  BookmarkFolderItems, DragItemDataType, DragItemType, DRAG_ITEM_TYPE,
-} from '~/interfaces/bookmark-info';
-import { onDeletedBookmarkFolderFunction } from '~/interfaces/ui';
+import type { BookmarkFolderItems, DragItemDataType, DragItemType } from '~/interfaces/bookmark-info';
+import { DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
+import type { onDeletedBookmarkFolderFunction } from '~/interfaces/ui';
 import { useBookmarkFolderDeleteModal } from '~/stores/modal';
 
 import { BookmarkFolderItemControl } from './BookmarkFolderItemControl';
@@ -42,7 +40,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
   } = props;
 
   const {
-    name, _id: folderId, children, parent, bookmarks,
+    name, _id: folderId, childFolder, parent, bookmarks,
   } = bookmarkFolder;
 
   const [targetFolder, setTargetFolder] = useState<string | null>(folderId);
@@ -52,7 +50,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
 
   const { open: openDeleteBookmarkFolderModal } = useBookmarkFolderDeleteModal();
 
-  const childrenExists = hasChildren(children);
+  const childrenExists = hasChildren({ childFolder, bookmarks });
 
   const paddingLeft = BASE_FOLDER_PADDING * level;
 
@@ -65,14 +63,14 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
   const onPressEnterHandlerForRename = useCallback(async(folderName: string) => {
     try {
       // TODO: do not use any type
-      await updateBookmarkFolder(folderId, folderName, parent as any, children);
+      await updateBookmarkFolder(folderId, folderName, parent as any, childFolder);
       bookmarkFolderTreeMutation();
       setIsRenameAction(false);
     }
     catch (err) {
       toastError(err);
     }
-  }, [bookmarkFolderTreeMutation, children, folderId, parent]);
+  }, [bookmarkFolderTreeMutation, childFolder, folderId, parent]);
 
   // Create new folder / subfolder handler
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
@@ -99,7 +97,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     if (dragItemType === DRAG_ITEM_TYPE.FOLDER) {
       try {
         if (item.bookmarkFolder != null) {
-          await updateBookmarkFolder(item.bookmarkFolder._id, item.bookmarkFolder.name, bookmarkFolder._id, item.bookmarkFolder.children);
+          await updateBookmarkFolder(item.bookmarkFolder._id, item.bookmarkFolder.name, bookmarkFolder._id, item.bookmarkFolder.childFolder);
           bookmarkFolderTreeMutation();
         }
       }
@@ -129,7 +127,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
       // Maximum folder hierarchy of 2 levels
       // If the drop source folder has child folders, the drop source folder cannot be moved because the drop source folder hierarchy is already 2.
       // If the destination folder has a parent, the source folder cannot be moved because the destination folder hierarchy is already 2.
-      if (item.bookmarkFolder.children.length !== 0 || bookmarkFolder.parent != null) {
+      if (item.bookmarkFolder.childFolder.length !== 0 || bookmarkFolder.parent != null) {
         return false;
       }
 
@@ -142,9 +140,15 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     return true;
   };
 
+  const triangleBtnClassName = (isOpen: boolean, childrenExists: boolean): string => {
+    if (!childrenExists) {
+      return 'grw-foldertree-triangle-btn btn px-0 opacity-25';
+    }
+    return `grw-foldertree-triangle-btn btn px-0 ${isOpen ? 'grw-foldertree-open' : ''}`;
+  };
 
   const renderChildFolder = () => {
-    return isOpen && children?.map((childFolder) => {
+    return isOpen && childFolder?.map((childFolder) => {
       return (
         <div key={childFolder._id} className="grw-foldertree-item-children">
           <BookmarkFolderItem
@@ -201,13 +205,13 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
 
   const onClickMoveToRootHandlerForBookmarkFolderItemControl = useCallback(async() => {
     try {
-      await updateBookmarkFolder(bookmarkFolder._id, bookmarkFolder.name, null, bookmarkFolder.children);
+      await updateBookmarkFolder(bookmarkFolder._id, bookmarkFolder.name, null, bookmarkFolder.childFolder);
       bookmarkFolderTreeMutation();
     }
     catch (err) {
       toastError(err);
     }
-  }, [bookmarkFolder._id, bookmarkFolder.children, bookmarkFolder.name, bookmarkFolderTreeMutation]);
+  }, [bookmarkFolder._id, bookmarkFolder.childFolder, bookmarkFolder.name, bookmarkFolderTreeMutation]);
 
   return (
     <div id={`grw-bookmark-folder-item-${folderId}`} className="grw-foldertree-item-container">
@@ -221,22 +225,20 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
         isDropable={isDropable}
       >
         <li
-          className="list-group-item list-group-item-action border-0 py-0 pe-3 d-flex align-items-center"
+          className="list-group-item list-group-item-action border-0 py-2 d-flex align-items-center rounded"
           onClick={loadChildFolder}
           style={{ paddingLeft }}
         >
           <div className="grw-triangle-container d-flex justify-content-center">
-            {childrenExists && (
-              <button
-                type="button"
-                className={`grw-foldertree-triangle-btn btn ${isOpen ? 'grw-foldertree-open' : ''}`}
-                onClick={loadChildFolder}
-              >
-                <div className="d-flex justify-content-center">
-                  <span className="material-symbols-outlined">arrow_right</span>
-                </div>
-              </button>
-            )}
+            <button
+              type="button"
+              className={triangleBtnClassName(isOpen, childrenExists)}
+              onClick={loadChildFolder}
+            >
+              <div className="d-flex justify-content-center">
+                <span className="material-symbols-outlined">arrow_right</span>
+              </div>
+            </button>
           </div>
           <div>
             <FolderIcon isOpen={isOpen} />
@@ -249,7 +251,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
             />
           ) : (
             <>
-              <div className="grw-foldertree-title-anchor ps-2">
+              <div className="grw-foldertree-title-anchor ps-1">
                 <p className="text-truncate m-auto ">{name}</p>
               </div>
             </>

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

@@ -156,7 +156,7 @@ export const BookmarkFolderMenu = (props: BookmarkFolderMenuProps): JSX.Element
                     isSelected={selectedItem === folder._id}
                   />
                 </div>
-                {folder.children?.map(child => (
+                {folder.childFolder?.map(child => (
                   <div key={child._id}>
                     <div
                       className="dropdown-item grw-bookmark-folder-menu-item grw-bookmark-folder-menu-item-folder-second list-group-item list-group-item-action"

+ 4 - 4
apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss

@@ -33,6 +33,9 @@ $grw-bookmark-item-padding-left: 35px;
       .grw-visible-on-hover {
         display: block;
       }
+      .page-list-meta {
+        display: none;
+      }
     }
 
     .grw-foldertree-triangle-btn {
@@ -54,10 +57,7 @@ $grw-bookmark-item-padding-left: 35px;
 
   .grw-foldertree-item-container {
     .grw-triangle-container {
-      // TODO: ignore width frickering
-      // https://redmine.weseek.co.jp/issues/130828
-      // min-width: 35px;
-      height: 40px;
+      height:30px;
     }
 
     .grw-bookmark-item-list{

+ 2 - 2
apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx

@@ -6,7 +6,7 @@ import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 
 import { toastSuccess } from '~/client/util/toastr';
-import { OnDeletedFunction } from '~/interfaces/ui';
+import type { OnDeletedFunction } from '~/interfaces/ui';
 import {
   useSWRxUserBookmarks, useSWRMUTxCurrentUserBookmarks,
 } from '~/stores/bookmark';
@@ -103,7 +103,7 @@ export const BookmarkFolderTree: React.FC<Props> = (props: Props) => {
 
   return (
     <div className={`grw-folder-tree-container ${styles['grw-folder-tree-container']}`}>
-      <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group px-2 py-2`}>
+      <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group py-2`}>
         {bookmarkFolders?.map((bookmarkFolder) => {
           return (
             <BookmarkFolderItem

+ 3 - 3
apps/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -39,7 +39,7 @@ type Props = {
 
 export const BookmarkItem = (props: Props): JSX.Element => {
   const BASE_FOLDER_PADDING = 15;
-  const BASE_BOOKMARK_PADDING = 20;
+  const BASE_BOOKMARK_PADDING = 16;
 
   const { t } = useTranslation();
   const router = useRouter();
@@ -56,7 +56,7 @@ export 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 paddingLeft = BASE_BOOKMARK_PADDING + (BASE_FOLDER_PADDING * (level + 1));
+  const paddingLeft = BASE_BOOKMARK_PADDING + (BASE_FOLDER_PADDING * (level));
   const dragItem: Partial<DragItemDataType> = {
     ...bookmarkedPage, parentFolder,
   };
@@ -148,7 +148,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
       useDragMode={isOperable}
     >
       <li
-        className="grw-bookmark-item-list list-group-item list-group-item-action border-0 py-0 me-auto d-flex align-items-center"
+        className="grw-bookmark-item-list list-group-item list-group-item-action border-0 py-0 pe-1 me-auto d-flex align-items-center rounded"
         key={bookmarkedPage._id}
         id={bookmarkItemId}
         style={{ paddingLeft }}

+ 2 - 2
apps/app/src/components/Icons/FolderIcon.tsx

@@ -9,10 +9,10 @@ export const FolderIcon = (props: Props): JSX.Element => {
   return (
     <>
       {!isOpen ? (
-        <span className="material-symbols-outlined">folder_open</span>
+        <span className="material-symbols-outlined">folder</span>
 
       ) : (
-        <span className="material-symbols-outlined">folder</span>
+        <span className="material-symbols-outlined">folder_open</span>
       )
       }
     </>

+ 2 - 2
apps/app/src/components/PageList/PageListItemS.tsx

@@ -35,14 +35,14 @@ export const PageListItemS = (props: PageListItemSProps): JSX.Element => {
       <UserPicture user={page.lastUpdateUser} noLink={noLink} />
       {isNarrowView ? (
         <Clamp lines={2}>
-          <div className={`mx-2 ${styles['page-title']} ${noLink ? 'text-break' : ''}`}>
+          <div className={`mx-1 ${styles['page-title']} ${noLink ? 'text-break' : ''}`}>
             {pagePathElement}
           </div>
         </Clamp>
       ) : (
         pagePathElement
       )}
-      <span className="ms-2">
+      <span className="ms-1">
         <PageListMeta page={page} shouldSpaceOutIcon />
       </span>
     </>

+ 1 - 1
apps/app/src/components/Sidebar/Bookmarks.tsx

@@ -17,7 +17,7 @@ export const Bookmarks = () : JSX.Element => {
         <h4 className="mb-0 py-4">{t('Bookmarks')}</h4>
       </div>
       {isGuestUser ? (
-        <h4 className="ps-3">
+        <h4 className="fs-6">
           { t('Not available for guest') }
         </h4>
       ) : (

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

@@ -37,8 +37,8 @@ export const BookmarkContents = (): JSX.Element => {
   }, [mutateBookmarkFolders]);
 
   return (
-    <div className="ms-3">
-      <div className="col-8 mb-2">
+    <div>
+      <div className="mb-2">
         <button
           type="button"
           className="btn btn-outline-secondary rounded-pill d-flex justify-content-start align-middle"

+ 1 - 1
apps/app/src/interfaces/bookmark-info.ts

@@ -24,7 +24,7 @@ export interface IBookmarkFolder {
 
 export interface BookmarkFolderItems extends IBookmarkFolder {
   _id: string;
-  children: BookmarkFolderItems[];
+  childFolder: BookmarkFolderItems[];
   bookmarks: BookmarkedPage[];
 }
 

+ 8 - 9
apps/app/src/server/models/bookmark-folder.ts

@@ -1,10 +1,9 @@
 import type { IPageHasId } from '@growi/core';
 import { objectIdUtils } from '@growi/core/dist/utils';
-import monggoose, {
-  Types, Document, Model, Schema,
-} from 'mongoose';
+import type { Types, Document, Model } from 'mongoose';
+import monggoose, { Schema } from 'mongoose';
 
-import { BookmarkFolderItems, IBookmarkFolder } from '~/interfaces/bookmark-info';
+import type { BookmarkFolderItems, IBookmarkFolder } from '~/interfaces/bookmark-info';
 
 import loggerFactory from '../../utils/logger';
 import { getOrCreateModel } from '../util/mongoose-utils';
@@ -21,13 +20,13 @@ export interface BookmarkFolderDocument extends Document {
   owner: Types.ObjectId
   parent?: Types.ObjectId | undefined
   bookmarks?: Types.ObjectId[],
-  children?: BookmarkFolderDocument[]
+  childFolder?: BookmarkFolderDocument[]
 }
 
 export interface BookmarkFolderModel extends Model<BookmarkFolderDocument>{
   createByParameters(params: IBookmarkFolder): Promise<BookmarkFolderDocument>
   deleteFolderAndChildren(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}>
-  updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string | null, children: BookmarkFolderItems[]): Promise<BookmarkFolderDocument>
+  updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string | null, childFolder: BookmarkFolderItems[]): Promise<BookmarkFolderDocument>
   insertOrUpdateBookmarkedPage(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null): Promise<BookmarkFolderDocument | null>
   updateBookmark(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId| string): Promise<BookmarkFolderDocument | null>
 }
@@ -51,7 +50,7 @@ const bookmarkFolderSchema = new Schema<BookmarkFolderDocument, BookmarkFolderMo
   toObject: { virtuals: true },
 });
 
-bookmarkFolderSchema.virtual('children', {
+bookmarkFolderSchema.virtual('childFolder', {
   ref: 'BookmarkFolder',
   localField: '_id',
   foreignField: 'parent',
@@ -108,7 +107,7 @@ bookmarkFolderSchema.statics.updateBookmarkFolder = async function(
     bookmarkFolderId: string,
     name: string,
     parentId: string | null,
-    children: BookmarkFolderItems[],
+    childFolder: BookmarkFolderItems[],
 ):
  Promise<BookmarkFolderDocument> {
   const updateFields: {name: string, parent: Types.ObjectId | null} = {
@@ -127,7 +126,7 @@ bookmarkFolderSchema.statics.updateBookmarkFolder = async function(
     if (parentFolder?.parent != null) {
       throw new Error('Update bookmark folder failed');
     }
-    if (children.length !== 0) {
+    if (childFolder.length !== 0) {
       throw new Error('Update bookmark folder failed');
     }
   }

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

@@ -1,8 +1,8 @@
 import { ErrorV3 } from '@growi/core/dist/models';
 import { body } from 'express-validator';
-import { Types } from 'mongoose';
+import type { Types } from 'mongoose';
 
-import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { InvalidParentBookmarkFolderError } from '~/server/models/errors';
 import { serializeBookmarkSecurely } from '~/server/models/serializers/bookmark-serializer';
@@ -25,7 +25,7 @@ const validator = {
           throw new Error('Maximum folder hierarchy of 2 levels');
         }
       }),
-    body('children').optional().isArray().withMessage('Children must be an array'),
+    body('childFolder').optional().isArray().withMessage('Children must be an array'),
     body('bookmarkFolderId').optional().isMongoId().withMessage('Bookark Folder ID must be a valid mongo ID'),
   ],
   bookmarkPage: [
@@ -73,7 +73,7 @@ module.exports = (crowi) => {
         parentFolderId?: Types.ObjectId | string,
     ) => {
       const folders = await BookmarkFolder.find({ owner: userId, parent: parentFolderId })
-        .populate('children')
+        .populate('childFolder')
         .populate({
           path: 'bookmarks',
           model: 'Bookmark',
@@ -90,7 +90,7 @@ module.exports = (crowi) => {
       const returnValue: BookmarkFolderItems[] = [];
 
       const promises = folders.map(async(folder: BookmarkFolderItems) => {
-        const children = await getBookmarkFolders(userId, folder._id);
+        const childFolder = 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.
@@ -101,7 +101,7 @@ module.exports = (crowi) => {
           name: folder.name,
           owner: folder.owner,
           bookmarks,
-          children,
+          childFolder,
           parent: folder.parent,
         };
         return res;
@@ -139,10 +139,10 @@ module.exports = (crowi) => {
 
   router.put('/', accessTokenParser, loginRequiredStrictly, validator.bookmarkFolder, async(req, res) => {
     const {
-      bookmarkFolderId, name, parent, children,
+      bookmarkFolderId, name, parent, childFolder,
     } = req.body;
     try {
-      const bookmarkFolder = await BookmarkFolder.updateBookmarkFolder(bookmarkFolderId, name, parent, children);
+      const bookmarkFolder = await BookmarkFolder.updateBookmarkFolder(bookmarkFolderId, name, parent, childFolder);
       return res.apiv3({ bookmarkFolder });
     }
     catch (err) {