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

Merge remote-tracking branch 'origin/master' into support/devide-core-package

Yuki Takei 1 год назад
Родитель
Сommit
6a1caa8eac

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

@@ -67,7 +67,7 @@ export const BookmarkFolderMenu = (props: BookmarkFolderMenuProps): JSX.Element
     if (isOpen && bookmarkFolders != null) {
       bookmarkFolders.forEach((bookmarkFolder) => {
         bookmarkFolder.bookmarks.forEach((bookmark) => {
-          if (bookmark.page._id === pageId) {
+          if (bookmark.page?._id === pageId) {
             setSelectedItem(bookmarkFolder._id);
           }
         });

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

@@ -121,9 +121,8 @@ export const BookmarkFolderTree: React.FC<Props> = (props: Props) => {
           );
         })}
         {userBookmarks?.map(userBookmark => (
-          <div key={userBookmark._id} className="grw-foldertree-item-container grw-root-bookmarks">
+          <div key={userBookmark?._id} className="grw-foldertree-item-container grw-root-bookmarks">
             <BookmarkItem
-              key={userBookmark._id}
               isReadOnlyUser={!!isReadOnlyUser}
               isOperable={props.isOperable}
               bookmarkedPage={userBookmark}

+ 24 - 8
apps/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -28,7 +28,7 @@ import { DragAndDropWrapper } from './DragAndDropWrapper';
 type Props = {
   isReadOnlyUser: boolean
   isOperable: boolean,
-  bookmarkedPage: IPageHasId,
+  bookmarkedPage: IPageHasId | null,
   level: number,
   parentFolder: BookmarkFolderItems | null,
   canMoveToRoot: boolean,
@@ -50,17 +50,17 @@ export const BookmarkItem = (props: Props): JSX.Element => {
   const { open: openPutBackPageModal } = usePutBackPageModal();
   const [isRenameInputShown, setRenameInputShown] = useState(false);
 
-  const { data: pageInfo, mutate: mutatePageInfo } = useSWRxPageInfo(bookmarkedPage._id);
+  const { data: pageInfo, mutate: mutatePageInfo } = useSWRxPageInfo(bookmarkedPage?._id);
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
-  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));
   const dragItem: Partial<DragItemDataType> = {
     ...bookmarkedPage, parentFolder,
   };
 
   const onClickMoveToRootHandler = useCallback(async() => {
+    if (bookmarkedPage == null) return;
+
     try {
       await addBookmarkToFolder(bookmarkedPage._id, null);
       bookmarkFolderTreeMutation();
@@ -68,7 +68,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
     catch (err) {
       toastError(err);
     }
-  }, [bookmarkFolderTreeMutation, bookmarkedPage._id]);
+  }, [bookmarkFolderTreeMutation, bookmarkedPage]);
 
   const bookmarkMenuItemClickHandler = useCallback(async(pageId: string, shouldBookmark: boolean) => {
     if (shouldBookmark) {
@@ -90,6 +90,9 @@ export const BookmarkItem = (props: Props): JSX.Element => {
   }, []);
 
   const rename = useCallback(async(inputText: string) => {
+    if (bookmarkedPage == null) return;
+
+
     if (inputText.trim() === '') {
       return cancel();
     }
@@ -111,9 +114,11 @@ export const BookmarkItem = (props: Props): JSX.Element => {
       setRenameInputShown(true);
       toastError(err);
     }
-  }, [bookmarkedPage.path, bookmarkedPage._id, bookmarkedPage.revision, cancel, bookmarkFolderTreeMutation, mutatePageInfo]);
+  }, [bookmarkedPage, cancel, bookmarkFolderTreeMutation, mutatePageInfo]);
 
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
+    if (bookmarkedPage == null) return;
+
     if (bookmarkedPage._id == null || bookmarkedPage.path == null) {
       throw Error('_id and path must not be null.');
     }
@@ -128,9 +133,11 @@ export const BookmarkItem = (props: Props): JSX.Element => {
     };
 
     onClickDeleteMenuItemHandler(pageToDelete);
-  }, [bookmarkedPage._id, bookmarkedPage.path, bookmarkedPage.revision, onClickDeleteMenuItemHandler]);
+  }, [bookmarkedPage, onClickDeleteMenuItemHandler]);
 
   const putBackClickHandler = useCallback(() => {
+    if (bookmarkedPage == null) return;
+
     const { _id: pageId, path } = bookmarkedPage;
     const putBackedHandler = async() => {
       try {
@@ -148,6 +155,15 @@ export const BookmarkItem = (props: Props): JSX.Element => {
     openPutBackPageModal({ pageId, path }, { onPutBacked: putBackedHandler });
   }, [bookmarkedPage, openPutBackPageModal, bookmarkFolderTreeMutation, router, mutateCurrentPage, t]);
 
+  if (bookmarkedPage == null) {
+    return <></>;
+  }
+
+  const dPagePath = new DevidedPagePath(bookmarkedPage.path, false, true);
+  const { latter: pageTitle, former: formerPagePath } = dPagePath;
+
+  const bookmarkItemId = `bookmark-item-${bookmarkedPage._id}`;
+
   return (
     <DragAndDropWrapper
       item={dragItem}

+ 16 - 14
apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx

@@ -3,9 +3,6 @@ import React, {
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
-import {
-  UncontrolledButtonDropdown, DropdownMenu, DropdownToggle, DropdownItem,
-} from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
@@ -44,28 +41,33 @@ export const PageTreeHeader = memo(({ isWipPageShown, onWipPageShownChange }: He
     <>
       <SidebarHeaderReloadButton onClick={() => mutate()} />
 
-      <UncontrolledButtonDropdown className="me-1">
-        <DropdownToggle color="transparent" className="p-0 border-0">
+      <div className="me-1">
+        <button
+          color="transparent"
+          className="btn p-0 border-0"
+          type="button"
+          data-bs-toggle="dropdown"
+          data-bs-auto-close="outside"
+          aria-expanded="false"
+        >
           <span className="material-symbols-outlined">more_horiz</span>
-        </DropdownToggle>
+        </button>
 
-        <DropdownMenu container="body">
-          <DropdownItem onClick={onWipPageShownChange} className="">
+        <ul className="dropdown-menu">
+          <li className="dropdown-item" onClick={onWipPageShownChange}>
             <div className="form-check form-switch">
               <input
-                id="wipPageVisibility"
                 className="form-check-input"
                 type="checkbox"
                 checked={isWipPageShown}
-                onChange={() => {}}
               />
-              <label className="form-label form-check-label text-muted mb-0" htmlFor="wipPageVisibility">
+              <label className="form-label form-check-label text-muted mb-0">
                 {t('sidebar_header.show_wip_page')}
               </label>
             </div>
-          </DropdownItem>
-        </DropdownMenu>
-      </UncontrolledButtonDropdown>
+          </li>
+        </ul>
+      </div>
     </>
   );
 });

+ 24 - 2
apps/app/src/components/Sidebar/SidebarNav/SidebarNav.tsx

@@ -1,7 +1,9 @@
-import React, { memo } from 'react';
+import React, { memo, useMemo } from 'react';
 
 import type { SidebarContentsType } from '~/interfaces/ui';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 
+import { NotAvailableForReadOnlyUser } from '../../NotAvailableForReadOnlyUser';
 import { PageCreateButton } from '../PageCreateButton';
 
 import { PrimaryItems } from './PrimaryItems';
@@ -16,9 +18,29 @@ export type SidebarNavProps = {
 export const SidebarNav = memo((props: SidebarNavProps) => {
   const { onPrimaryItemHover } = props;
 
+  const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
+
+  const renderedPageCreateButton = useMemo(() => {
+    if (isGuestUser) {
+      return <></>;
+    }
+
+    if (isReadOnlyUser) {
+      return (
+        <NotAvailableForReadOnlyUser>
+          <PageCreateButton />
+        </NotAvailableForReadOnlyUser>
+      );
+    }
+
+    return <PageCreateButton />;
+  }, [isGuestUser, isReadOnlyUser]);
+
   return (
     <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`}>
-      <PageCreateButton />
+
+      {renderedPageCreateButton}
 
       <div className="grw-sidebar-nav-primary-container" data-vrt-blackout-sidebar-nav>
         <PrimaryItems onItemHover={onPrimaryItemHover} />

+ 7 - 7
apps/app/src/features/growi-plugin/server/services/growi-plugin/growi-plugin.integ.ts

@@ -59,27 +59,27 @@ describe('Installing a GROWI theme plugin', () => {
   it('install() should success', async() => {
     // when
     const result = await growiPluginService.install({
-      url: 'https://github.com/weseek/growi-plugin-theme-welcome-to-fumiya-room',
+      url: 'https://github.com/weseek/growi-plugin-theme-vivid-internet',
     });
-    const count = await GrowiPlugin.count({ 'meta.name': 'growi-plugin-theme-welcome-to-fumiya-room' });
+    const count = await GrowiPlugin.count({ 'meta.name': 'growi-plugin-theme-vivid-internet' });
 
     // expect
-    expect(result).toEqual('growi-plugin-theme-welcome-to-fumiya-room');
+    expect(result).toEqual('growi-plugin-theme-vivid-internet');
     expect(count).toBe(1);
     expect(fs.existsSync(path.join(
       PLUGIN_STORING_PATH,
       'weseek',
-      'growi-plugin-theme-welcome-to-fumiya-room',
+      'growi-plugin-theme-vivid-internet',
     ))).toBeTruthy();
   });
 
   it('findThemePlugin() should return data with metadata and manifest', async() => {
     // confirm
-    const count = await GrowiPlugin.count({ 'meta.name': 'growi-plugin-theme-welcome-to-fumiya-room' });
+    const count = await GrowiPlugin.count({ 'meta.name': 'growi-plugin-theme-vivid-internet' });
     expect(count).toBe(1);
 
     // when
-    const results = await growiPluginService.findThemePlugin('welcome-to-fumiya-room');
+    const results = await growiPluginService.findThemePlugin('vivid-internet');
 
     // expect
     expect(results).not.toBeNull();
@@ -88,7 +88,7 @@ describe('Installing a GROWI theme plugin', () => {
     expect(results.themeMetadata).not.toBeNull();
     expect(results.themeHref).not.toBeNull();
     expect(results.themeHref
-      .startsWith('/static/plugins/weseek/growi-plugin-theme-welcome-to-fumiya-room/dist/assets/style.')).toBeTruthy();
+      .startsWith('/static/plugins/weseek/growi-plugin-theme-vivid-internet/dist/assets/style.')).toBeTruthy();
   });
 
 });

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

@@ -9,7 +9,7 @@ export interface IBookmarkInfo {
 
 export interface BookmarkedPage {
   _id: string,
-  page: IPageHasId,
+  page: IPageHasId | null,
   user: Ref<IUser>,
   createdAt: Date,
 }

+ 3 - 3
apps/app/src/server/service/page/index.ts

@@ -1869,8 +1869,7 @@ class PageService implements IPageService {
   }
 
   async deleteCompletelyOperation(pageIds, pagePaths): Promise<void> {
-    // Delete Bookmarks, Attachments, Revisions, Pages and emit delete
-    const Bookmark = this.crowi.model('Bookmark');
+    // Delete Attachments, Revisions, Pages and emit delete
     const Page = this.crowi.model('Page');
     const Revision = this.crowi.model('Revision');
 
@@ -1878,7 +1877,6 @@ class PageService implements IPageService {
     const attachments = await Attachment.find({ page: { $in: pageIds } });
 
     await Promise.all([
-      Bookmark.deleteMany({ page: { $in: pageIds } }),
       Comment.deleteMany({ page: { $in: pageIds } }),
       PageTagRelation.deleteMany({ relatedPage: { $in: pageIds } }),
       ShareLink.deleteMany({ relatedPage: { $in: pageIds } }),
@@ -1886,6 +1884,8 @@ class PageService implements IPageService {
       Page.deleteMany({ _id: { $in: pageIds } }),
       PageRedirect.deleteMany({ $or: [{ fromPath: { $in: pagePaths } }, { toPath: { $in: pagePaths } }] }),
       attachmentService.removeAllAttachments(attachments),
+
+      // Leave bookmarks without deleting -- 2024.05.17 Yuki Takei
     ]);
   }
 

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

@@ -1,11 +1,12 @@
 import type { IUser, IPageHasId } from '@growi/core';
-import useSWR, { SWRResponse } from 'swr';
+import type { SWRResponse } from 'swr';
+import useSWR from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
 
 
 import { apiv3Get } from '../client/util/apiv3-client';
-import { IBookmarkInfo } from '../interfaces/bookmark-info';
+import type { IBookmarkInfo } from '../interfaces/bookmark-info';
 
 import { useCurrentUser } from './context';
 
@@ -16,32 +17,24 @@ export const useSWRxBookmarkedUsers = (pageId: string | null): SWRResponse<IUser
   );
 };
 
-export const useSWRxUserBookmarks = (userId: string | null): SWRResponse<IPageHasId[], Error> => {
+export const useSWRxUserBookmarks = (userId: string | null): SWRResponse<(IPageHasId | null)[], Error> => {
   return useSWRImmutable(
     userId != null ? `/bookmarks/${userId}` : null,
     endpoint => apiv3Get(endpoint).then((response) => {
       const { userRootBookmarks } = response.data;
-      return userRootBookmarks.map((item) => {
-        return {
-          ...item.page,
-        };
-      });
+      return userRootBookmarks.map(item => item.page); // page will be null if the page is deleted
     }),
   );
 };
 
-export const useSWRMUTxCurrentUserBookmarks = (): SWRMutationResponse<IPageHasId[], Error> => {
+export const useSWRMUTxCurrentUserBookmarks = (): SWRMutationResponse<(IPageHasId | null)[], Error> => {
   const { data: currentUser } = useCurrentUser();
 
   return useSWRMutation(
     currentUser != null ? `/bookmarks/${currentUser?._id}` : null,
     endpoint => apiv3Get(endpoint).then((response) => {
       const { userRootBookmarks } = response.data;
-      return userRootBookmarks.map((item) => {
-        return {
-          ...item.page,
-        };
-      });
+      return userRootBookmarks.map(item => item.page); // page will be null if the page is deleted
     }),
   );
 };

+ 3 - 1
apps/app/test/integration/service/page.test.js

@@ -870,7 +870,9 @@ describe('PageService', () => {
       test('deleteCompletelyOperation', async() => {
         await crowi.pageService.deleteCompletelyOperation([parentForDeleteCompletely._id], [parentForDeleteCompletely.path], { });
 
-        expect(deleteManyBookmarkSpy).toHaveBeenCalledWith({ page: { $in: [parentForDeleteCompletely._id] } });
+        // bookmarks should not be deleted
+        expect(deleteManyBookmarkSpy).not.toHaveBeenCalled();
+
         expect(deleteManyCommentSpy).toHaveBeenCalledWith({ page: { $in: [parentForDeleteCompletely._id] } });
         expect(deleteManyPageTagRelationSpy).toHaveBeenCalledWith({ relatedPage: { $in: [parentForDeleteCompletely._id] } });
         expect(deleteManyShareLinkSpy).toHaveBeenCalledWith({ relatedPage: { $in: [parentForDeleteCompletely._id] } });

+ 2 - 2
apps/app/test/integration/service/v5.public-page.test.ts

@@ -2103,7 +2103,7 @@ describe('PageService page operations with only public pages', () => {
       const deletedRevisions = await Revision.find({ pageId: { $in: [parentPage._id, grandchildPage._id] } });
       const tags = await Tag.find({ _id: { $in: [tag1?._id, tag2?._id] } });
       const deletedPageTagRelations = await PageTagRelation.find({ _id: { $in: [pageTagRelation1?._id, pageTagRelation2?._id] } });
-      const deletedBookmarks = await Bookmark.find({ _id: bookmark._id });
+      const remainingBookmarks = await Bookmark.find({ _id: bookmark._id });
       const deletedComments = await Comment.find({ _id: comment._id });
       const deletedPageRedirects = await PageRedirect.find({ _id: { $in: [pageRedirect1._id, pageRedirect2._id] } });
       const deletedShareLinks = await ShareLink.find({ _id: { $in: [shareLink1._id, shareLink2._id] } });
@@ -2117,7 +2117,7 @@ describe('PageService page operations with only public pages', () => {
       // PageTagRelation should be null
       expect(deletedPageTagRelations.length).toBe(0);
       // bookmark should be null
-      expect(deletedBookmarks.length).toBe(0);
+      expect(remainingBookmarks.length).toBe(1);
       // comment should be null
       expect(deletedComments.length).toBe(0);
       // pageRedirect should be null