Przeglądaj źródła

Merge branch 'feat/gw7840-implement-rename-from-bookmark-sidebar' into feat/gw7841-implement-delete-from-bookmark-sidebar

Mudana-Grune 3 lat temu
rodzic
commit
fd303948a6

+ 85 - 58
packages/app/src/components/Sidebar/Bookmarks.tsx

@@ -8,41 +8,40 @@ import nodePath from 'path';
 import { DevidedPagePath, pathUtils } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
-import type { SWRInfiniteResponse } from 'swr/infinite';
-
 import { unbookmark } from '~/client/services/page-operation';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
-import { apiv3Put } from '~/client/util/apiv3-client';
 import { IPageHasId, IPageInfoAll, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { OnDeletedFunction } from '~/interfaces/ui';
-import LinkedPagePath from '~/models/linked-page-path';
-import { useSWRInifiniteBookmarkedPage } from '~/stores/bookmark';
-import { useCurrentUser } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
+import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
+import { useCurrentUser, useIsGuestUser } from '~/stores/context';
+import loggerFactory from '~/utils/logger';
 
 import ClosableTextInput, { AlertInfo, AlertType } from '../Common/ClosableTextInput';
 import { MenuItemType, PageItemControl } from '../Common/Dropdown/PageItemControl';
-import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
 
-import InfiniteScroll from './InfiniteScroll';
+const logger = loggerFactory('growi:BookmarkList');
+// TODO: Remove pagination and apply  scrolling (not infinity)
+const ACTIVE_PAGE = 1;
 
 type Props = {
   page: IPageHasId,
-  swr: SWRInfiniteResponse
+  refreshBookmarkList: () => void
 }
-const BookmarksItem:FC<Props> = (props: Props) : JSX.Element => {
+
+const BookmarksItem = (props: Props) => {
   const { t } = useTranslation();
-  const { page, swr } = props;
+  const { page, refreshBookmarkList } = props;
+  const [isRenameInputShown, setRenameInputShown] = useState(false);
   const dPagePath = new DevidedPagePath(page.path, false, true);
-  const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+  const { latter: pageTitle, former: formerPagePath } = dPagePath;
   const bookmarkItemId = `bookmark-item-${page._id}`;
-  const [isRenameInputShown, setRenameInputShown] = useState(false);
-  const { open: openDeleteModal } = usePageDeleteModal();
+
 
   const bookmarkMenuItemClickHandler = useCallback(async() => {
     await unbookmark(page._id);
-    swr.mutate();
-  }, [swr, page]);
+    refreshBookmarkList();
+  }, [page, refreshBookmarkList]);
 
   const renameMenuItemClickHandler = useCallback(() => {
     setRenameInputShown(true);
@@ -59,10 +58,9 @@ const BookmarksItem:FC<Props> = (props: Props) : JSX.Element => {
     return null;
   };
 
-  const onPressEnterForRenameHandler = useCallback(async(inputText: string) => {
+  const pressEnterForRenameHandler = (async(inputText: string) => {
     const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
     const newPagePath = nodePath.resolve(parentPath, inputText);
-
     if (newPagePath === page.path) {
       setRenameInputShown(false);
       return;
@@ -75,14 +73,14 @@ const BookmarksItem:FC<Props> = (props: Props) : JSX.Element => {
         revisionId: page.revision,
         newPagePath,
       });
-      swr.mutate();
+      refreshBookmarkList();
       toastSuccess(t('renamed_pages', { path: page.path }));
     }
     catch (err) {
       setRenameInputShown(true);
       toastError(err);
     }
-  }, [swr, page, t]);
+  });
 
 
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
@@ -99,7 +97,7 @@ const BookmarksItem:FC<Props> = (props: Props) : JSX.Element => {
         else {
           toastSuccess(t('deleted_pages', { path }));
         }
-        swr.mutate();
+        refreshBookmarkList();
       };
       openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
     };
@@ -118,26 +116,25 @@ const BookmarksItem:FC<Props> = (props: Props) : JSX.Element => {
     };
 
     onClickDeleteMenuItem(pageToDelete);
-  }, [page, swr, openDeleteModal, t]);
+  }, [page, openDeleteModal, t]);
 
   return (
     <>
-      <li className="list-group-item py-3 px-2" id={bookmarkItemId}>
-        <div className="d-flex w-100 justify-content-between">
+      <div className="d-flex justify-content-between" key={page._id}>
+        <li className="list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center" id={bookmarkItemId}>
           { isRenameInputShown ? (
             <ClosableTextInput
               value={nodePath.basename(page.path ?? '')}
               placeholder={t('Input page name')}
               onClickOutside={() => { setRenameInputShown(false) }}
-              onPressEnter={onPressEnterForRenameHandler}
+              onPressEnter={pressEnterForRenameHandler}
               inputValidator={inputValidator}
             />
           ) : (
-            <h5 className="my-0">
-              <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
-            </h5>
+            <a href={`/${page._id}`} className="grw-bookmarks-title-anchor flex-grow-1">
+              <p className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageTitle}</p>
+            </a>
           )}
-
           <PageItemControl
             pageId={page._id}
             isEnableActions
@@ -150,56 +147,86 @@ const BookmarksItem:FC<Props> = (props: Props) : JSX.Element => {
               <i className="icon-options fa fa-rotate-90 p-1"></i>
             </DropdownToggle>
           </PageItemControl>
-
           <UncontrolledTooltip
             modifiers={{ preventOverflow: { boundariesElement: 'window' } }}
             autohide={false}
-            placement="left"
+            placement="right"
             target={bookmarkItemId}
           >
-            {page.path}
+            { formerPagePath || '/' }
           </UncontrolledTooltip>
-        </div>
-      </li>
-
+        </li>
+      </div>
     </>
   );
-
 };
 
+
 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);
+  const { data: isGuestUser } = useIsGuestUser();
+  const [pages, setPages] = useState<IPageHasId[]>([]);
+  const page = ACTIVE_PAGE;
+
+  const getMyBookmarkList = useCallback(async() => {
+    try {
+      const res = await apiv3Get(`/bookmarks/${currentUser?._id}`, { page });
+      const { paginationResult } = res.data;
+      setPages(paginationResult.docs.map((page) => {
+        return {
+          ...page.page,
+        };
+      }));
+    }
+    catch (error) {
+      logger.error('failed to fetch data', error);
+      toastError(error, 'Error occurred in bookmark page list');
+    }
+  }, [currentUser, page]);
 
   useEffect(() => {
-    swr.mutate();
-  }, []);
+    getMyBookmarkList();
+  }, [getMyBookmarkList]);
+
+  const generateBookmarkList = () => {
+    return (
+      <ul className="grw-bookmarks-list list-group p-3">
+        <div className="grw-bookmarks-item-container">
+          { pages.map((page) => {
+            return (
+              <BookmarksItem key={page._id} page={page} refreshBookmarkList={getMyBookmarkList} />
+            );
+          })}
+        </div>
+      </ul>
+    );
+  };
+
+  const renderBookmarksItem = () => {
+    if (pages?.length === 0) {
+      return (
+        <h3 className="pl-3">
+          { t('No bookmarks yet') }
+        </h3>
+      );
+    }
+    return generateBookmarkList();
+  };
 
   return (
     <>
       <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') : (
-          <div>
-            <ul className="list-group list-group-flush">
-              <InfiniteScroll
-                swrInifiniteResponse={swr}
-                isReachingEnd={isReachingEnd}
-              >
-                {paginationResult => paginationResult?.docs.map(data => (
-                  <BookmarksItem key={data.page._id} page={data.page} swr={swr} />
-                ))
-                }
-              </InfiniteScroll>
-            </ul>
-          </div>
-        )}
-      </div>
+      { isGuestUser
+        ? (
+          <h3 className="pl-3">
+            { t('Not available for guest') }
+          </h3>
+        ) : renderBookmarksItem()
+      }
+
     </>
   );
 

+ 2 - 2
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -258,7 +258,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     setRenameInputShown(true);
   }, []);
 
-  const onPressEnterForRenameHandler = async(inputText: string) => {
+  const pressEnterForRenameHandler = async(inputText: string) => {
     const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
     const newPagePath = nodePath.resolve(parentPath, inputText);
 
@@ -443,7 +443,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
               value={nodePath.basename(page.path ?? '')}
               placeholder={t('Input page name')}
               onClickOutside={() => { setRenameInputShown(false) }}
-              onPressEnter={onPressEnterForRenameHandler}
+              onPressEnter={pressEnterForRenameHandler}
               inputValidator={inputValidator}
             />
           )

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

@@ -1,6 +1,5 @@
 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';
@@ -18,17 +17,3 @@ 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,
-    },
-  );
-};

+ 0 - 17
packages/app/src/styles/_bookmark-list.scss

@@ -1,17 +0,0 @@
-.grw-bookmarks-list{
-  .list-group-item {
-    .grw-visible-on-hover {
-      display: none;
-    }
-
-    &:hover {
-      .grw-visible-on-hover {
-        display: block;
-      }
-
-      .grw-count-badge {
-        display: none;
-      }
-    }
-  }
-}

+ 38 - 0
packages/app/src/styles/_bookmarks.scss

@@ -0,0 +1,38 @@
+$grw-sidebar-content-header-height: 58px;
+$grw-sidebar-content-footer-height: 50px;
+
+.grw-bookmarks-list {
+  min-height: calc(100vh - ($grw-navbar-height + $grw-navbar-border-width + $grw-sidebar-content-header-height + $grw-sidebar-content-footer-height));
+
+  .btn-page-item-control {
+    .icon-plus::before {
+      font-size: 18px;
+    }
+  }
+
+  .list-group-item {
+    .grw-visible-on-hover {
+      display: none;
+    }
+
+    &:hover {
+      .grw-visible-on-hover {
+        display: block;
+      }
+    }
+
+    .grw-bookmarks-title-anchor {
+      width: 100%;
+      overflow: hidden;
+      text-decoration: none;
+    }
+
+  }
+  .grw-bookmarks-item-container {
+    .list-group-item {
+      min-width: 35px;
+      height: 40px;
+    }
+  }
+
+}

+ 1 - 1
packages/app/src/styles/style-app.scss

@@ -36,6 +36,7 @@
 // growi component
 @import 'admin';
 @import 'attachments';
+@import 'bookmarks';
 @import 'comment';
 @import 'comment_growi';
 @import 'drawio';
@@ -59,7 +60,6 @@
 @import 'page-accessories-modal';
 @import 'page-path';
 @import 'page-tree';
-@import 'bookmark-list';
 @import 'page';
 @import 'page-presentation';
 @import 'page-history';

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

@@ -300,6 +300,19 @@ ul.pagination {
       box-shadow: none !important;
     }
   }
+
+  // bookmarks
+  .grw-bookmarks-list {
+    @include override-list-group-item-for-pagetree(
+      $color-sidebar-context,
+      lighten($bgcolor-sidebar-context, 8%),
+      lighten($bgcolor-sidebar-context, 15%),
+      darken($color-sidebar-context, 15%),
+      darken($color-sidebar-context, 10%),
+      lighten($bgcolor-sidebar-context, 18%),
+      lighten($bgcolor-sidebar-context, 24%)
+    );
+  }
   .private-legacy-pages-link {
     &:hover {
       background: $bgcolor-list-hover;

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

@@ -198,6 +198,19 @@ $dropdown-link-active-bg: $bgcolor-dropdown-link-active;
       @include button-outline-svg-icon-variant($gray-400, $primary);
     }
   }
+
+  // bookmark
+  .grw-bookmarks-list {
+    @include override-list-group-item-for-pagetree(
+      $color-sidebar-context,
+      darken($bgcolor-sidebar-context, 5%),
+      darken($bgcolor-sidebar-context, 12%),
+      lighten($color-sidebar-context, 10%),
+      lighten($color-sidebar-context, 8%),
+      darken($bgcolor-sidebar-context, 15%),
+      darken($bgcolor-sidebar-context, 24%)
+    );
+  }
   .private-legacy-pages-link {
     &:hover {
       background: $bgcolor-list-hover;

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

@@ -319,21 +319,6 @@ ul.pagination {
     }
   }
 
-  .grw-bookmarks-list {
-    .list-group {
-      .list-group-item {
-        background-color: transparent;
-        &:hover{
-          background-color: $bgcolor-list-hover;
-        }
-      }
-    }
-    .list-group-flush {
-      .list-group-item {
-        border: none;
-      }
-    }
-  }
 }
 
 /*