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

Merge pull request #7631 from weseek/feat/120698-121330-add-is-read-only-user

feat: Add isReadOnlyUser condition to client side
Ryoji Shimizu 2 лет назад
Родитель
Сommit
ccb8012acd
37 измененных файлов с 244 добавлено и 142 удалено
  1. 4 1
      apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  2. 0 1
      apps/app/src/components/Bookmarks/BookmarkFolderMenu.tsx
  3. 4 0
      apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx
  4. 3 1
      apps/app/src/components/Bookmarks/BookmarkItem.tsx
  5. 7 6
      apps/app/src/components/Common/Dropdown/PageItemControl.tsx
  6. 3 1
      apps/app/src/components/DescendantsPageList.tsx
  7. 1 0
      apps/app/src/components/IdenticalPathPage.tsx
  8. 17 11
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  9. 17 14
      apps/app/src/components/Navbar/GrowiNavbar.tsx
  10. 3 3
      apps/app/src/components/Navbar/GrowiSubNavigation.tsx
  11. 8 7
      apps/app/src/components/Navbar/SubNavButtons.tsx
  12. 0 1
      apps/app/src/components/NotAvailableForGuest.tsx
  13. 28 0
      apps/app/src/components/NotAvailableForReadOnlyUser.tsx
  14. 14 11
      apps/app/src/components/Page/RenderTagLabels.tsx
  15. 3 3
      apps/app/src/components/Page/TagLabels.tsx
  16. 4 3
      apps/app/src/components/PageAccessoriesModal.tsx
  17. 8 5
      apps/app/src/components/PageAttachment.tsx
  18. 14 11
      apps/app/src/components/PageComment.tsx
  19. 11 8
      apps/app/src/components/PageComment/CommentEditor.tsx
  20. 3 1
      apps/app/src/components/PageList/PageList.tsx
  21. 3 1
      apps/app/src/components/PageList/PageListItemL.tsx
  22. 4 3
      apps/app/src/components/PageStatusAlert.tsx
  23. 5 2
      apps/app/src/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx
  24. 5 2
      apps/app/src/components/ReactMarkdownComponents/Header.tsx
  25. 5 2
      apps/app/src/components/ReactMarkdownComponents/TableWithEditButton.tsx
  26. 17 14
      apps/app/src/components/SearchPage.tsx
  27. 5 2
      apps/app/src/components/SearchPage/SearchPageBase.tsx
  28. 3 1
      apps/app/src/components/SearchPage/SearchResultList.tsx
  29. 4 2
      apps/app/src/components/Sidebar/PageTree.tsx
  30. 15 9
      apps/app/src/components/Sidebar/PageTree/Item.tsx
  31. 3 1
      apps/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  32. 5 4
      apps/app/src/components/TrashPageList.tsx
  33. 3 2
      apps/app/src/pages/trash.page.tsx
  34. 4 3
      apps/app/src/stores/context.tsx
  35. 3 2
      apps/app/src/stores/editor.tsx
  36. 5 2
      apps/app/src/stores/page.tsx
  37. 3 2
      apps/app/src/stores/ui.tsx

+ 4 - 1
apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -23,6 +23,7 @@ import { BookmarkItem } from './BookmarkItem';
 import { DragAndDropWrapper } from './DragAndDropWrapper';
 
 type BookmarkFolderItemProps = {
+  isReadOnlyUser: boolean
   bookmarkFolder: BookmarkFolderItems
   isOpen?: boolean
   level: number
@@ -36,7 +37,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
   const BASE_FOLDER_PADDING = 15;
   const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const {
-    bookmarkFolder, isOpen: _isOpen = false, level, root, isUserHomePage,
+    isReadOnlyUser, bookmarkFolder, isOpen: _isOpen = false, level, root, isUserHomePage,
     onClickDeleteBookmarkHandler, bookmarkFolderTreeMutation,
   } = props;
 
@@ -146,6 +147,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
         <div key={childFolder._id} className="grw-foldertree-item-children">
           <BookmarkFolderItem
             key={childFolder._id}
+            isReadOnlyUser={isReadOnlyUser}
             bookmarkFolder={childFolder}
             level={level + 1}
             root={root}
@@ -163,6 +165,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
       return (
         <BookmarkItem
           key={bookmark._id}
+          isReadOnlyUser={isReadOnlyUser}
           bookmarkedPage={bookmark.page}
           level={level + 1}
           parentFolder={bookmarkFolder}

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

@@ -103,7 +103,6 @@ export const BookmarkFolderMenu: React.FC<{children?: React.ReactNode}> = ({ chi
     }
   }, [mutateBookmarkFolders, isBookmarked, currentPage, mutateBookmarkInfo, mutateUserBookmarks, toggleBookmarkHandler]);
 
-  console.log(selectedItem);
   const renderBookmarkMenuItem = () => {
     return (
       <>

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

@@ -8,6 +8,7 @@ import { IPageToDeleteWithMeta } from '~/interfaces/page';
 import { OnDeletedFunction } from '~/interfaces/ui';
 import { useSWRxCurrentUserBookmarks, useSWRBookmarkInfo } from '~/stores/bookmark';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
+import { useIsReadOnlyUser } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 
@@ -26,6 +27,7 @@ export const BookmarkFolderTree: React.FC<{isUserHomePage?: boolean}> = ({ isUse
   // const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const { t } = useTranslation();
 
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPage } = useSWRxCurrentPage();
   const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
   const { data: bookmarkFolders, mutate: mutateBookmarkFolders } = useSWRxBookmarkFolderAndChild();
@@ -90,6 +92,7 @@ export const BookmarkFolderTree: React.FC<{isUserHomePage?: boolean}> = ({ isUse
           return (
             <BookmarkFolderItem
               key={bookmarkFolder._id}
+              isReadOnlyUser={!!isReadOnlyUser}
               bookmarkFolder={bookmarkFolder}
               isOpen={false}
               level={0}
@@ -104,6 +107,7 @@ export const BookmarkFolderTree: React.FC<{isUserHomePage?: boolean}> = ({ isUse
           <div key={userBookmark._id} className="grw-foldertree-item-container grw-root-bookmarks">
             <BookmarkItem
               key={userBookmark._id}
+              isReadOnlyUser={!!isReadOnlyUser}
               bookmarkedPage={userBookmark}
               level={0}
               parentFolder={null}

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

@@ -22,6 +22,7 @@ import { BookmarkMoveToRootBtn } from './BookmarkMoveToRootBtn';
 import { DragAndDropWrapper } from './DragAndDropWrapper';
 
 type Props = {
+  isReadOnlyUser: boolean
   bookmarkedPage: IPageHasId,
   level: number,
   parentFolder: BookmarkFolderItems | null,
@@ -37,7 +38,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
   const { t } = useTranslation();
 
   const {
-    bookmarkedPage, onClickDeleteBookmarkHandler,
+    isReadOnlyUser, bookmarkedPage, onClickDeleteBookmarkHandler,
     parentFolder, level, canMoveToRoot, bookmarkFolderTreeMutation,
   } = props;
 
@@ -133,6 +134,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
           <PageItemControl
             pageId={bookmarkedPage._id}
             isEnableActions
+            isReadOnlyUser={isReadOnlyUser}
             pageInfo={fetchedPageInfo}
             forceHideMenuItems={[MenuItemType.DUPLICATE]}
             onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}

+ 7 - 6
apps/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -38,6 +38,7 @@ export type AdditionalMenuItemsRendererProps = { pageInfo: IPageInfoAll };
 type CommonProps = {
   pageInfo?: IPageInfoAll,
   isEnableActions?: boolean,
+  isReadOnlyUser?: boolean,
   forceHideMenuItems?: ForceHideMenuItems,
 
   onClickBookmarkMenuItem?: (pageId: string, newValue?: boolean) => Promise<void>,
@@ -64,7 +65,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
   const { t } = useTranslation('');
 
   const {
-    pageId, isLoading, pageInfo, isEnableActions, forceHideMenuItems, operationProcessData,
+    pageId, isLoading, pageInfo, isEnableActions, isReadOnlyUser, forceHideMenuItems, operationProcessData,
     onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem,
     onClickRevertMenuItem, onClickPathRecoveryMenuItem,
     additionalMenuItemOnTopRenderer: AdditionalMenuItemsOnTop,
@@ -176,7 +177,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
         ) }
 
         {/* Move/Rename */}
-        { !forceHideMenuItems?.includes(MenuItemType.RENAME) && isEnableActions && pageInfo.isMovable && (
+        { !forceHideMenuItems?.includes(MenuItemType.RENAME) && isEnableActions && !isReadOnlyUser && pageInfo.isMovable && (
           <DropdownItem
             onClick={renameItemClickedHandler}
             data-testid="open-page-move-rename-modal-btn"
@@ -188,7 +189,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
         ) }
 
         {/* Duplicate */}
-        { !forceHideMenuItems?.includes(MenuItemType.DUPLICATE) && isEnableActions && (
+        { !forceHideMenuItems?.includes(MenuItemType.DUPLICATE) && isEnableActions && !isReadOnlyUser && (
           <DropdownItem
             onClick={duplicateItemClickedHandler}
             data-testid="open-page-duplicate-modal-btn"
@@ -200,7 +201,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
         ) }
 
         {/* Revert */}
-        { !forceHideMenuItems?.includes(MenuItemType.REVERT) && isEnableActions && pageInfo.isRevertible && (
+        { !forceHideMenuItems?.includes(MenuItemType.REVERT) && isEnableActions && !isReadOnlyUser && pageInfo.isRevertible && (
           <DropdownItem
             onClick={revertItemClickedHandler}
             className="grw-page-control-dropdown-item"
@@ -218,7 +219,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
         ) }
 
         {/* PathRecovery */}
-        { !forceHideMenuItems?.includes(MenuItemType.PATH_RECOVERY) && isEnableActions && shouldShowPathRecoveryButton && (
+        { !forceHideMenuItems?.includes(MenuItemType.PATH_RECOVERY) && isEnableActions && !isReadOnlyUser && shouldShowPathRecoveryButton && (
           <DropdownItem
             onClick={pathRecoveryItemClickedHandler}
             className="grw-page-control-dropdown-item"
@@ -230,7 +231,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
 
         {/* divider */}
         {/* Delete */}
-        { !forceHideMenuItems?.includes(MenuItemType.DELETE) && isEnableActions && pageInfo.isMovable && (
+        { !forceHideMenuItems?.includes(MenuItemType.DELETE) && isEnableActions && !isReadOnlyUser && pageInfo.isMovable && (
           <>
             { showDeviderBeforeDelete && <DropdownItem divider /> }
             <DropdownItem

+ 3 - 1
apps/app/src/components/DescendantsPageList.tsx

@@ -11,7 +11,7 @@ import {
 import { IPagingResult } from '~/interfaces/paging-result';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import {
-  useIsGuestUser, useIsSharedUser,
+  useIsGuestUser, useIsReadOnlyUser, useIsSharedUser,
 } from '~/stores/context';
 import {
   mutatePageTree,
@@ -45,6 +45,7 @@ const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
   } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
   const pageIds = pagingResult?.items?.map(page => page._id);
   const { injectTo } = useSWRxPageInfoForList(pageIds, null, true, true);
@@ -107,6 +108,7 @@ const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
       <PageList
         pages={pageWithMetas}
         isEnableActions={!isGuestUser}
+        isReadOnlyUser={!!isReadOnlyUser}
         forceHideMenuItems={forceHideMenuItems}
         onPagesDeleted={pageDeletedHandler}
         onPagePutBacked={pagePutBackedHandler}

+ 1 - 0
apps/app/src/components/IdenticalPathPage.tsx

@@ -75,6 +75,7 @@ export const IdenticalPathPage = (): JSX.Element => {
                 page={pageWithMeta}
                 isSelected={false}
                 isEnableActions
+                isReadOnlyUser={false}
                 showPageUpdatedTime
               />
             );

+ 17 - 11
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -17,7 +17,7 @@ import {
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
   useCurrentPathname,
-  useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId, useIsContainerFluid, useIsIdenticalPath,
+  useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId, useIsContainerFluid, useIsIdenticalPath,
 } from '~/stores/context';
 import { usePageTagsForEditors } from '~/stores/editor';
 import {
@@ -81,6 +81,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
   } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
 
   const { open: openPresentationModal } = usePagePresentationModal();
@@ -117,7 +118,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
       */}
       <DropdownItem
         onClick={() => openAccessoriesModal(PageAccessoriesModalContents.PageHistory)}
-        disabled={isGuestUser || isSharedUser}
+        disabled={!!isGuestUser || !!isSharedUser}
         data-testid="open-page-accessories-modal-btn-with-history-tab"
         className="grw-page-control-dropdown-item"
       >
@@ -138,7 +139,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         {t('attachment_data')}
       </DropdownItem>
 
-      { !isGuestUser && !isSharedUser && (
+      { !isGuestUser && !isReadOnlyUser && !isSharedUser && (
         <NotAvailable isDisabled={isLinkSharingDisabled ?? false} title="Disabled by admin">
           <DropdownItem
             onClick={() => openAccessoriesModal(PageAccessoriesModalContents.ShareLink)}
@@ -212,6 +213,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: isNotFound } = useIsNotFound();
   const { data: isIdenticalPath } = useIsIdenticalPath();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isContainerFluid } = useIsContainerFluid();
 
@@ -336,9 +338,11 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       if (revisionId == null || pageId == null) {
         return (
           <>
-            <CreateTemplateMenuItems
+            {!isReadOnlyUser
+            && <CreateTemplateMenuItems
               onClickTemplateMenuItem={templateMenuItemClickHandler}
             />
+            }
           </>);
       }
       return (
@@ -348,10 +352,12 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
             revisionId={revisionId}
             isLinkSharingDisabled={isLinkSharingDisabled}
           />
-          <DropdownItem divider />
-          <CreateTemplateMenuItems
-            onClickTemplateMenuItem={templateMenuItemClickHandler}
-          />
+          {!isReadOnlyUser && <>
+            <DropdownItem divider />
+            <CreateTemplateMenuItems
+              onClickTemplateMenuItem={templateMenuItemClickHandler}
+            /></>
+          }
         </>
       );
     };
@@ -384,7 +390,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
             {isAbleToChangeEditorMode && (
               <PageEditorModeManager
                 onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
-                isBtnDisabled={isGuestUser}
+                isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
                 editorMode={editorMode}
               />
             )}
@@ -407,7 +413,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
           ) }
         </div>
 
-        {path != null && currentUser != null && (
+        {path != null && currentUser != null && !isReadOnlyUser && (
           <CreateTemplateModal
             path={path}
             isOpen={isPageTemplateModalShown}
@@ -429,7 +435,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       pageId={currentPage?._id}
       showDrawerToggler={isDrawerMode}
       showTagLabel={isAbleToShowTagLabel}
-      isGuestUser={isGuestUser}
+      isTagLabelsDisabled={!!isGuestUser || !!isReadOnlyUser}
       isDrawerMode={isDrawerMode}
       isCompactMode={isCompactMode}
       tags={isViewMode ? tagsInfoData?.tags : tagsForEditors}

+ 17 - 14
apps/app/src/components/Navbar/GrowiNavbar.tsx

@@ -9,7 +9,7 @@ import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
 
 import {
-  useIsSearchPage, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useIsDefaultLogo,
+  useIsSearchPage, useIsGuestUser, useIsReadOnlyUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useIsDefaultLogo,
 } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
@@ -31,6 +31,7 @@ const NavbarRight = memo((): JSX.Element => {
 
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
   // ripple
   const newButtonRef = useRef(null);
@@ -47,18 +48,20 @@ const NavbarRight = memo((): JSX.Element => {
           <InAppNotificationDropdown />
         </li>
 
-        <li className="nav-item d-none d-md-block">
-          <button
-            className="px-md-3 nav-link btn-create-page border-0 bg-transparent"
-            type="button"
-            ref={newButtonRef}
-            data-testid="newPageBtn"
-            onClick={() => openCreateModal(currentPagePath || '')}
-          >
-            <i className="icon-pencil mr-2"></i>
-            <span className="d-none d-lg-block">{ t('commons:New') }</span>
-          </button>
-        </li>
+        {!isReadOnlyUser
+          && <li className="nav-item d-none d-md-block">
+            <button
+              className="px-md-3 nav-link btn-create-page border-0 bg-transparent"
+              type="button"
+              ref={newButtonRef}
+              data-testid="newPageBtn"
+              onClick={() => openCreateModal(currentPagePath || '')}
+            >
+              <i className="icon-pencil mr-2"></i>
+              <span className="d-none d-lg-block">{ t('commons:New') }</span>
+            </button>
+          </li>
+        }
 
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
           <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
@@ -69,7 +72,7 @@ const NavbarRight = memo((): JSX.Element => {
         </li>
       </>
     );
-  }, [t, isAuthenticated, openCreateModal, currentPagePath]);
+  }, [isReadOnlyUser, t, isAuthenticated, openCreateModal, currentPagePath]);
 
   const notAuthenticatedNavItem = useMemo(() => {
     return (

+ 3 - 3
apps/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -27,7 +27,7 @@ export type GrowiSubNavigationProps = {
   isNotFound?: boolean,
   showDrawerToggler?: boolean,
   showTagLabel?: boolean,
-  isGuestUser?: boolean,
+  isTagLabelsDisabled?: boolean,
   isDrawerMode?: boolean,
   isCompactMode?: boolean,
   tags?: string[],
@@ -43,7 +43,7 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
   const {
     pageId, pagePath,
     showDrawerToggler, showTagLabel,
-    isGuestUser, isDrawerMode, isCompactMode,
+    isTagLabelsDisabled, isDrawerMode, isCompactMode,
     tags, tagsUpdatedHandler,
     rightComponent: RightComponent,
     additionalClasses = [],
@@ -70,7 +70,7 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
           { (showTagLabel && !isCompactMode) && (
             <div className="grw-taglabels-container">
               { tags != null
-                ? <TagLabels tags={tags} isGuestUser={isGuestUser ?? false} tagsUpdateInvoked={tagsUpdatedHandler} />
+                ? <TagLabels tags={tags} isTagLabelsDisabled={isTagLabelsDisabled ?? false} tagsUpdateInvoked={tagsUpdatedHandler} />
                 : <TagLabelsSkeleton />
               }
             </div>

+ 8 - 7
apps/app/src/components/Navbar/SubNavButtons.tsx

@@ -10,7 +10,7 @@ import { toastError } from '~/client/util/toastr';
 import {
   IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
 } from '~/interfaces/page';
-import { useIsGuestUser } from '~/stores/context';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 
 import { useSWRBookmarkInfo } from '../../stores/bookmark';
@@ -90,6 +90,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId, shareLinkId);
 
@@ -104,7 +105,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   const seenUsers = usersList != null ? usersList.filter(({ _id }) => seenUserIds.includes(_id)).slice(0, 15) : [];
 
   const subscribeClickhandler = useCallback(async() => {
-    if (isGuestUser == null || isGuestUser) {
+    if (isGuestUser ?? true) {
       return;
     }
     if (!isIPageInfoForOperation(pageInfo)) {
@@ -116,7 +117,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
 
   const likeClickhandler = useCallback(async() => {
-    if (isGuestUser == null || isGuestUser) {
+    if (isGuestUser ?? true) {
       return;
     }
     if (!isIPageInfoForOperation(pageInfo)) {
@@ -127,7 +128,6 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
     mutatePageInfo();
   }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
 
-
   const duplicateMenuItemClickHandler = useCallback(async(_pageId: string): Promise<void> => {
     if (onClickDuplicateMenuItem == null || path == null) {
       return;
@@ -172,7 +172,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   }, [onClickDeleteMenuItem, pageId, pageInfo, path, revisionId]);
 
   const switchContentWidthClickHandler = useCallback(async(newValue: boolean) => {
-    if (onClickSwitchContentWidth == null || isGuestUser == null || isGuestUser) {
+    if (onClickSwitchContentWidth == null || (isGuestUser ?? true) || (isReadOnlyUser ?? true)) {
       return;
     }
     if (!isIPageInfoForEntity(pageInfo)) {
@@ -184,7 +184,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
     catch (err) {
       toastError(err);
     }
-  }, [isGuestUser, onClickSwitchContentWidth, pageId, pageInfo]);
+  }, [isGuestUser, isReadOnlyUser, onClickSwitchContentWidth, pageId, pageInfo]);
 
   const additionalMenuItemOnTopRenderer = useMemo(() => {
     if (!isIPageInfoForEntity(pageInfo)) {
@@ -245,8 +245,9 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
           pageId={pageId}
           pageInfo={pageInfo}
           isEnableActions={!isGuestUser}
+          isReadOnlyUser={!!isReadOnlyUser}
           forceHideMenuItems={forceHideMenuItemsWithBookmark}
-          additionalMenuItemOnTopRenderer={additionalMenuItemOnTopRenderer}
+          additionalMenuItemOnTopRenderer={!isReadOnlyUser ? additionalMenuItemOnTopRenderer : undefined}
           additionalMenuItemRenderer={additionalMenuItemRenderer}
           onClickRenameMenuItem={renameMenuItemClickHandler}
           onClickDuplicateMenuItem={duplicateMenuItemClickHandler}

+ 0 - 1
apps/app/src/components/NotAvailableForGuest.tsx

@@ -6,7 +6,6 @@ import { useIsGuestUser } from '~/stores/context';
 
 import { NotAvailable } from './NotAvailable';
 
-
 type NotAvailableForGuestProps = {
   children: JSX.Element
 }

+ 28 - 0
apps/app/src/components/NotAvailableForReadOnlyUser.tsx

@@ -0,0 +1,28 @@
+import React from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import { useIsReadOnlyUser } from '~/stores/context';
+
+import { NotAvailable } from './NotAvailable';
+
+export const NotAvailableForReadOnlyUser: React.FC<{
+  children: JSX.Element
+}> = React.memo(({ children }) => {
+  const { t } = useTranslation();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
+
+  const isDisabled = !!isReadOnlyUser;
+  const title = t('Not available for read only user');
+
+  return (
+    <NotAvailable
+      isDisabled={isDisabled}
+      title={title}
+      classNamePrefix="grw-not-available-for-read-only-user"
+    >
+      {children}
+    </NotAvailable>
+  );
+});
+NotAvailableForReadOnlyUser.displayName = 'NotAvailableForReadOnlyUser';

+ 14 - 11
apps/app/src/components/Page/RenderTagLabels.tsx

@@ -3,15 +3,16 @@ import React from 'react';
 import { useTranslation } from 'next-i18next';
 
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
+import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 
 type RenderTagLabelsProps = {
   tags: string[],
-  isGuestUser: boolean,
+  isTagLabelsDisabled: boolean,
   openEditorModal?: () => void,
 }
 
 const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
-  const { tags, isGuestUser, openEditorModal } = props;
+  const { tags, isTagLabelsDisabled, openEditorModal } = props;
   const { t } = useTranslation();
 
   function openEditorHandler() {
@@ -33,15 +34,17 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
         );
       })}
       <NotAvailableForGuest>
-        <div id="edit-tags-btn-wrapper-for-tooltip">
-          <a
-            className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isGuestUser && 'disabled'}`}
-            onClick={openEditorHandler}
-          >
-            { isTagsEmpty && <>{ t('Add tags for this page') }</>}
-            <i className={`icon-plus ${isTagsEmpty && 'ml-1'}`}/>
-          </a>
-        </div>
+        <NotAvailableForReadOnlyUser>
+          <div id="edit-tags-btn-wrapper-for-tooltip">
+            <a
+              className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isTagLabelsDisabled && 'disabled'}`}
+              onClick={openEditorHandler}
+            >
+              { isTagsEmpty && <>{ t('Add tags for this page') }</>}
+              <i className={`icon-plus ${isTagsEmpty && 'ml-1'}`}/>
+            </a>
+          </div>
+        </NotAvailableForReadOnlyUser>
       </NotAvailableForGuest>
     </>
 

+ 3 - 3
apps/app/src/components/Page/TagLabels.tsx

@@ -9,7 +9,7 @@ import styles from './TagLabels.module.scss';
 
 type Props = {
   tags?: string[],
-  isGuestUser: boolean,
+  isTagLabelsDisabled: boolean,
   tagsUpdateInvoked?: (tags: string[]) => Promise<void> | void,
 }
 
@@ -18,7 +18,7 @@ export const TagLabelsSkeleton = (): JSX.Element => {
 };
 
 export const TagLabels:FC<Props> = (props: Props) => {
-  const { tags, isGuestUser, tagsUpdateInvoked } = props;
+  const { tags, isTagLabelsDisabled, tagsUpdateInvoked } = props;
 
   const [isTagEditModalShown, setIsTagEditModalShown] = useState(false);
 
@@ -41,7 +41,7 @@ export const TagLabels:FC<Props> = (props: Props) => {
         <RenderTagLabels
           tags={tags}
           openEditorModal={openEditorModal}
-          isGuestUser={isGuestUser}
+          isTagLabelsDisabled={isTagLabelsDisabled}
         />
       </div>
       <TagEditModal

+ 4 - 3
apps/app/src/components/PageAccessoriesModal.tsx

@@ -6,7 +6,7 @@ import {
 } from 'reactstrap';
 
 import {
-  useDisableLinkSharing, useIsGuestUser, useIsSharedUser,
+  useDisableLinkSharing, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser,
 } from '~/stores/context';
 import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
 
@@ -34,6 +34,7 @@ const PageAccessoriesModal = (): JSX.Element => {
 
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isLinkSharingDisabled } = useDisableLinkSharing();
 
   const { data: status, mutate, close } = usePageAccessoriesModal();
@@ -93,10 +94,10 @@ const PageAccessoriesModal = (): JSX.Element => {
           return <ShareLink />;
         },
         i18n: t('share_links.share_link_management'),
-        isLinkEnabled: () => !isGuestUser && !isSharedUser && !isLinkSharingDisabled,
+        isLinkEnabled: () => !isGuestUser && !isReadOnlyUser && !isSharedUser && !isLinkSharingDisabled,
       },
     };
-  }, [t, close, sourceRevisionId, targetRevisionId, isGuestUser, isSharedUser, isLinkSharingDisabled]);
+  }, [t, close, sourceRevisionId, targetRevisionId, isGuestUser, isReadOnlyUser, isSharedUser, isLinkSharingDisabled]);
 
   const buttons = useMemo(() => (
     <div className="d-flex flex-nowrap">

+ 8 - 5
apps/app/src/components/PageAttachment.tsx

@@ -5,7 +5,7 @@ import React, {
 import { IAttachmentHasId } from '@growi/core';
 
 import { useSWRxAttachments } from '~/stores/attachment';
-import { useIsGuestUser } from '~/stores/context';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
 
 import { DeleteAttachmentModal } from './PageAttachment/DeleteAttachmentModal';
@@ -25,6 +25,9 @@ const PageAttachment = (): JSX.Element => {
   // Static SWRs
   const { data: pageId } = useCurrentPageId();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
+
+  const isPageAttachmentDisabled = !!isGuestUser || !!isReadOnlyUser;
 
   // States
   const [pageNumber, setPageNumber] = useState(1);
@@ -93,13 +96,13 @@ const PageAttachment = (): JSX.Element => {
         attachments={dataAttachments.attachments}
         inUse={inUseAttachmentsMap}
         onAttachmentDeleteClicked={onAttachmentDeleteClicked}
-        isUserLoggedIn={!isGuestUser}
+        isUserLoggedIn={!isPageAttachmentDisabled}
       />
     );
-  }, [dataAttachments, inUseAttachmentsMap, isGuestUser, onAttachmentDeleteClicked]);
+  }, [dataAttachments, inUseAttachmentsMap, isPageAttachmentDisabled, onAttachmentDeleteClicked]);
 
   const renderDeleteAttachmentModal = useCallback(() => {
-    if (isGuestUser) {
+    if (isPageAttachmentDisabled) {
       return <></>;
     }
 
@@ -120,7 +123,7 @@ const PageAttachment = (): JSX.Element => {
       />
     );
   // eslint-disable-next-line max-len
-  }, [attachmentToDelete, dataAttachments, deleteError, deleting, isGuestUser, onAttachmentDeleteClickedConfirmHandler, onToggleHandler]);
+  }, [attachmentToDelete, dataAttachments, deleteError, deleting, isPageAttachmentDisabled, onAttachmentDeleteClickedConfirmHandler, onToggleHandler]);
 
   const renderPaginationWrapper = useCallback(() => {
     if (dataAttachments == null || dataAttachments.attachments.length === 0) {

+ 14 - 11
apps/app/src/components/PageComment.tsx

@@ -15,6 +15,7 @@ import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { useSWRxPageComment } from '../stores/comment';
 
 import { NotAvailableForGuest } from './NotAvailableForGuest';
+import { NotAvailableForReadOnlyUser } from './NotAvailableForReadOnlyUser';
 import { Comment } from './PageComment/Comment';
 import { CommentEditor } from './PageComment/CommentEditor';
 import { DeleteCommentModal } from './PageComment/DeleteCommentModal';
@@ -177,17 +178,19 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
                   {(!isReadOnly && !showEditorIds.has(comment._id)) && (
                     <div className="d-flex flex-row-reverse">
                       <NotAvailableForGuest>
-                        <Button
-                          outline
-                          color="secondary"
-                          size="sm"
-                          className="btn-comment-reply"
-                          onClick={() => {
-                            setShowEditorIds(previousState => new Set(previousState.add(comment._id)));
-                          }}
-                        >
-                          <i className="icon-fw icon-action-undo"></i> Reply
-                        </Button>
+                        <NotAvailableForReadOnlyUser>
+                          <Button
+                            outline
+                            color="secondary"
+                            size="sm"
+                            className="btn-comment-reply"
+                            onClick={() => {
+                              setShowEditorIds(previousState => new Set(previousState.add(comment._id)));
+                            }}
+                          >
+                            <i className="icon-fw icon-action-undo"></i> Reply
+                          </Button>
+                        </NotAvailableForReadOnlyUser>
                       </NotAvailableForGuest>
                     </div>
                   )}

+ 11 - 8
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -21,6 +21,7 @@ import { useCurrentPagePath } from '~/stores/page';
 
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
+import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 import Editor from '../PageEditor/Editor';
 
 import { CommentPreview } from './CommentPreview';
@@ -235,14 +236,16 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     return (
       <div className="text-center">
         <NotAvailableForGuest>
-          <button
-            type="button"
-            className="btn btn-lg btn-link"
-            onClick={() => setIsReadyToUse(true)}
-            data-testid="open-comment-editor-button"
-          >
-            <i className="icon-bubble"></i> Add Comment
-          </button>
+          <NotAvailableForReadOnlyUser>
+            <button
+              type="button"
+              className="btn btn-lg btn-link"
+              onClick={() => setIsReadyToUse(true)}
+              data-testid="open-comment-editor-button"
+            >
+              <i className="icon-bubble"></i> Add Comment
+            </button>
+          </NotAvailableForReadOnlyUser>
         </NotAvailableForGuest>
       </div>
     );

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

@@ -14,6 +14,7 @@ import styles from './PageList.module.scss';
 type Props<M extends IPageInfoForEntity> = {
   pages: IPageWithMeta<M>[],
   isEnableActions?: boolean,
+  isReadOnlyUser: boolean,
   forceHideMenuItems?: ForceHideMenuItems,
   onPagesDeleted?: OnDeletedFunction,
   onPagePutBacked?: OnPutBackedFunction,
@@ -22,7 +23,7 @@ type Props<M extends IPageInfoForEntity> = {
 const PageList = (props: Props<IPageInfoForEntity>): JSX.Element => {
   const { t } = useTranslation();
   const {
-    pages, isEnableActions, forceHideMenuItems, onPagesDeleted, onPagePutBacked,
+    pages, isEnableActions, isReadOnlyUser, forceHideMenuItems, onPagesDeleted, onPagePutBacked,
   } = props;
 
   if (pages == null) {
@@ -40,6 +41,7 @@ const PageList = (props: Props<IPageInfoForEntity>): JSX.Element => {
       key={page.data._id}
       page={page}
       isEnableActions={isEnableActions}
+      isReadOnlyUser={isReadOnlyUser}
       forceHideMenuItems={forceHideMenuItems}
       onPageDeleted={onPagesDeleted}
       onPagePutBacked={onPagePutBacked}

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

@@ -38,6 +38,7 @@ type Props = {
   page: IPageWithSearchMeta | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>,
   isSelected?: boolean, // is item selected(focused)
   isEnableActions?: boolean,
+  isReadOnlyUser: boolean,
   forceHideMenuItems?: ForceHideMenuItems,
   showPageUpdatedTime?: boolean, // whether to show page's updated time at the top-right corner of item
   onCheckboxChanged?: (isChecked: boolean, pageId: string) => void,
@@ -50,7 +51,7 @@ type Props = {
 
 const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (props: Props, ref): JSX.Element => {
   const {
-    page: { data: pageData, meta: pageMeta }, isSelected, isEnableActions,
+    page: { data: pageData, meta: pageMeta }, isSelected, isEnableActions, isReadOnlyUser,
     forceHideMenuItems,
     showPageUpdatedTime,
     onClickItem, onCheckboxChanged, onPageDuplicated, onPageRenamed, onPageDeleted, onPagePutBacked,
@@ -259,6 +260,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
                   pageId={pageData._id}
                   pageInfo={isIPageInfoForListing(pageMeta) ? pageMeta : undefined}
                   isEnableActions={isEnableActions}
+                  isReadOnlyUser={isReadOnlyUser}
                   forceHideMenuItems={forceHideMenuItems}
                   onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
                   onClickRenameMenuItem={renameMenuItemClickHandler}

+ 4 - 3
apps/app/src/components/PageStatusAlert.tsx

@@ -3,7 +3,7 @@ import React, { useCallback, useMemo } from 'react';
 import { useTranslation } from 'next-i18next';
 import * as ReactDOMServer from 'react-dom/server';
 
-import { useIsGuestUser } from '~/stores/context';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useEditingMarkdown, useIsConflict } from '~/stores/editor';
 import {
   useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
@@ -32,7 +32,8 @@ export const PageStatusAlert = (): JSX.Element => {
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { open: openConflictDiffModal } = useConflictDiffModal();
   const { mutate: mutateEditorMode } = useEditorMode();
-  const { data: isGuest } = useIsGuestUser();
+  const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
   // store remote latest page data
   const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
@@ -154,7 +155,7 @@ export const PageStatusAlert = (): JSX.Element => {
     getContentsForDraftExistsAlert,
   ]);
 
-  if (isGuest || alertComponentContents == null) { return <></> }
+  if (!!isGuestUser || !!isReadOnlyUser || alertComponentContents == null) { return <></> }
 
   const { additionalClasses, label, btn } = alertComponentContents;
 

+ 5 - 2
apps/app/src/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx

@@ -9,7 +9,9 @@ import {
 } from '@growi/remark-drawio';
 import { useTranslation } from 'next-i18next';
 
-import { useIsGuestUser, useIsSharedUser, useShareLinkId } from '~/stores/context';
+import {
+  useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
+} from '~/stores/context';
 
 import '@growi/remark-drawio/dist/style.css';
 import styles from './DrawioViewerWithEditButton.module.scss';
@@ -27,6 +29,7 @@ export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps):
   const { bol, eol } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
 
@@ -52,7 +55,7 @@ export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps):
     }
   }, []);
 
-  const showEditButton = isRendered && !isGuestUser && !isSharedUser && shareLinkId == null;
+  const showEditButton = isRendered && !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
 
   return (
     <div className={`drawio-viewer-with-edit-button ${styles['drawio-viewer-with-edit-button']}`}>

+ 5 - 2
apps/app/src/components/ReactMarkdownComponents/Header.tsx

@@ -5,7 +5,9 @@ import EventEmitter from 'events';
 import { useRouter } from 'next/router';
 import { Element } from 'react-markdown/lib/rehype-filter';
 
-import { useIsGuestUser, useIsSharedUser, useShareLinkId } from '~/stores/context';
+import {
+  useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
+} from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
 import { NextLink } from './NextLink';
@@ -60,6 +62,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
   } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
 
@@ -107,7 +110,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
     };
   }, [activateByHash, router.events]);
 
-  const showEditButton = !isGuestUser && !isSharedUser && shareLinkId == null;
+  const showEditButton = !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
 
   return (
     <CustomTag id={id} className={`revision-head ${styles['revision-head']} ${isActive ? 'blink' : ''}`}>

+ 5 - 2
apps/app/src/components/ReactMarkdownComponents/TableWithEditButton.tsx

@@ -4,7 +4,9 @@ import EventEmitter from 'events';
 
 import { Element } from 'react-markdown/lib/rehype-filter';
 
-import { useIsGuestUser, useIsSharedUser, useShareLinkId } from '~/stores/context';
+import {
+  useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
+} from '~/stores/context';
 
 import styles from './TableWithEditButton.module.scss';
 
@@ -24,6 +26,7 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
   const { children, node, className } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
 
@@ -34,7 +37,7 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
     globalEmitter.emit('launchHandsonTableModal', bol, eol);
   }, [bol, eol]);
 
-  const showEditButton = !isGuestUser && !isSharedUser && shareLinkId == null;
+  const showEditButton = !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
 
   return (
     <div className={`editable-with-handsontable ${styles['editable-with-handsontable']}`}>

+ 17 - 14
apps/app/src/components/SearchPage.tsx

@@ -13,6 +13,7 @@ import { useIsSearchServiceReachable, useShowPageLimitationL } from '~/stores/co
 import { ISearchConditions, ISearchConfigurations, useSWRxSearch } from '~/stores/search';
 
 import { NotAvailableForGuest } from './NotAvailableForGuest';
+import { NotAvailableForReadOnlyUser } from './NotAvailableForReadOnlyUser';
 import PaginationWrapper from './PaginationWrapper';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import SearchControl from './SearchPage/SearchControl';
@@ -185,21 +186,23 @@ export const SearchPage = (): JSX.Element => {
 
     return (
       <NotAvailableForGuest>
-        <OperateAllControl
-          ref={selectAllControlRef}
-          isCheckboxDisabled={isDisabled}
-          onCheckboxChanged={selectAllCheckboxChangedHandler}
-        >
-          <button
-            type="button"
-            className="btn btn-outline-danger text-nowrap border-0 px-2"
-            disabled={isDisabled}
-            onClick={deleteAllButtonClickedHandler}
+        <NotAvailableForReadOnlyUser>
+          <OperateAllControl
+            ref={selectAllControlRef}
+            isCheckboxDisabled={isDisabled}
+            onCheckboxChanged={selectAllCheckboxChangedHandler}
           >
-            <i className="icon-fw icon-trash"></i>
-            {t('search_result.delete_all_selected_page')}
-          </button>
-        </OperateAllControl>
+            <button
+              type="button"
+              className="btn btn-outline-danger text-nowrap border-0 px-2"
+              disabled={isDisabled}
+              onClick={deleteAllButtonClickedHandler}
+            >
+              <i className="icon-fw icon-trash"></i>
+              {t('search_result.delete_all_selected_page')}
+            </button>
+          </OperateAllControl>
+        </NotAvailableForReadOnlyUser>
       </NotAvailableForGuest>
     );
   }, [deleteAllButtonClickedHandler, hitsCount, selectAllCheckboxChangedHandler, t]);

+ 5 - 2
apps/app/src/components/SearchPage/SearchPageBase.tsx

@@ -9,7 +9,9 @@ import { ISelectableAll } from '~/client/interfaces/selectable-all';
 import { toastSuccess } from '~/client/util/toastr';
 import { IFormattedSearchResult, IPageWithSearchMeta } from '~/interfaces/search';
 import { OnDeletedFunction } from '~/interfaces/ui';
-import { useIsGuestUser, useIsSearchServiceConfigured, useIsSearchServiceReachable } from '~/stores/context';
+import {
+  useIsGuestUser, useIsReadOnlyUser, useIsSearchServiceConfigured, useIsSearchServiceReachable,
+} from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
 import { mutatePageTree } from '~/stores/page-listing';
 
@@ -54,6 +56,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
   const searchResultListRef = useRef<ISelectableAll|null>(null);
 
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSearchServiceConfigured } = useIsSearchServiceConfigured();
   const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
 
@@ -206,7 +209,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
             <SearchResultContent
               pageWithMeta={selectedPageWithMeta}
               highlightKeywords={highlightKeywords}
-              showPageControlDropdown={!isGuestUser}
+              showPageControlDropdown={!(isGuestUser || isReadOnlyUser)}
               forceHideMenuItems={forceHideMenuItems}
             />
           )}

+ 3 - 1
apps/app/src/components/SearchPage/SearchResultList.tsx

@@ -11,7 +11,7 @@ import {
   IPageInfoForListing, IPageWithMeta, isIPageInfoForListing,
 } from '~/interfaces/page';
 import { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
-import { useIsGuestUser } from '~/stores/context';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { mutatePageTree, useSWRxPageInfoForList } from '~/stores/page-listing';
 import { mutateSearching } from '~/stores/search';
 
@@ -41,6 +41,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     .map(page => page.data._id);
 
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: idToPageInfo } = useSWRxPageInfoForList(pageIdsWithNoSnippet, null, true, true);
 
   const itemsRef = useRef<(ISelectable|null)[]>([]);
@@ -131,6 +132,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
             ref={c => itemsRef.current[i] = c}
             page={page}
             isEnableActions={!isGuestUser}
+            isReadOnlyUser={!!isReadOnlyUser}
             isSelected={page.data._id === selectedPageId}
             forceHideMenuItems={forceHideMenuItems}
             onClickItem={clickItemHandler}

+ 4 - 2
apps/app/src/components/Sidebar/PageTree.tsx

@@ -2,7 +2,7 @@ import React, { FC, memo } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import { useTargetAndAncestors, useIsGuestUser } from '~/stores/context';
+import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 
@@ -24,6 +24,7 @@ const PageTree: FC = memo(() => {
   const { t } = useTranslation();
 
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPath } = useCurrentPagePath();
   const { data: targetId } = useCurrentPageId();
   const { data: targetAndAncestorsData } = useTargetAndAncestors();
@@ -68,12 +69,13 @@ const PageTree: FC = memo(() => {
       <PageTreeHeader />
       <ItemsTree
         isEnableActions={!isGuestUser}
+        isReadOnlyUser={!!isReadOnlyUser}
         targetPath={path}
         targetPathOrId={targetPathOrId}
         targetAndAncestorsData={targetAndAncestorsData}
       />
 
-      {!isGuestUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
+      {!isGuestUser && !isReadOnlyUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
         <div className="grw-pagetree-footer border-top py-3 w-100">
           <div className="private-legacy-pages-link px-3 py-2">
             <PrivateLegacyPagesLink />

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

@@ -18,6 +18,7 @@ import { ValidationTarget } from '~/client/util/input-validator';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
 import { TriangleIcon } from '~/components/Icons/TriangleIcon';
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
+import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnlyUser';
 import {
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';
@@ -40,6 +41,7 @@ const logger = loggerFactory('growi:cli:Item');
 
 interface ItemProps {
   isEnableActions: boolean
+  isReadOnlyUser: boolean
   itemNode: ItemNode
   targetPathOrId?: Nullable<string>
   isOpen?: boolean
@@ -109,7 +111,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const { t } = useTranslation();
   const {
     itemNode, targetPathOrId, isOpen: _isOpen = false,
-    onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions,
+    onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser,
   } = props;
 
   const { page, children } = itemNode;
@@ -486,6 +488,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             <PageItemControl
               pageId={page._id}
               isEnableActions={isEnableActions}
+              isReadOnlyUser={isReadOnlyUser}
               onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
               onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
               onClickRenameMenuItem={renameMenuItemClickHandler}
@@ -505,14 +508,16 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
 
         {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
           <NotAvailableForGuest>
-            <button
-              id='page-create-button-in-page-tree'
-              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>
+            <NotAvailableForReadOnlyUser>
+              <button
+                id='page-create-button-in-page-tree'
+                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>
+            </NotAvailableForReadOnlyUser>
           </NotAvailableForGuest>
         )}
       </li>
@@ -534,6 +539,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           <div key={node.page._id} className="grw-pagetree-item-children">
             <Item
               isEnableActions={isEnableActions}
+              isReadOnlyUser={isReadOnlyUser}
               itemNode={node}
               isOpen={false}
               targetPathOrId={targetPathOrId}

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

@@ -90,6 +90,7 @@ const isSecondStageRenderingCondition = (condition: RenderingCondition|SecondSta
 
 type ItemsTreeProps = {
   isEnableActions: boolean
+  isReadOnlyUser: boolean
   targetPath: string
   targetPathOrId?: Nullable<string>
   targetAndAncestorsData?: TargetAndAncestors
@@ -100,7 +101,7 @@ type ItemsTreeProps = {
  */
 const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const {
-    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions,
+    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions, isReadOnlyUser,
   } = props;
 
   const { t } = useTranslation();
@@ -278,6 +279,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
           itemNode={initialItemNode}
           isOpen
           isEnableActions={isEnableActions}
+          isReadOnlyUser={isReadOnlyUser}
           onRenamed={onRenamed}
           onClickDuplicateMenuItem={onClickDuplicateMenuItem}
           onClickDeleteMenuItem={onClickDeleteMenuItem}

+ 5 - 4
apps/app/src/components/TrashPageList.tsx

@@ -8,7 +8,7 @@ import {
   IPageHasId,
 } from '~/interfaces/page';
 import { IPagingResult } from '~/interfaces/paging-result';
-import { useShowPageLimitationXL } from '~/stores/context';
+import { useIsReadOnlyUser, useShowPageLimitationXL } from '~/stores/context';
 import { useEmptyTrashModal } from '~/stores/modal';
 import { useSWRxPageInfoForList, useSWRxPageList } from '~/stores/page-listing';
 
@@ -28,9 +28,10 @@ const convertToIDataWithMeta = (page) => {
 
 const useEmptyTrashButton = () => {
 
+  const { t } = useTranslation();
   const { data: limit } = useShowPageLimitationXL();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: pagingResult, mutate: mutatePageLists } = useSWRxPageList('/trash', 1, limit);
-  const { t } = useTranslation();
   const { open: openEmptyTrashModal } = useEmptyTrashModal();
 
   const pageIds = pagingResult?.items?.map(page => page._id);
@@ -59,8 +60,8 @@ const useEmptyTrashButton = () => {
   }, [deletablePages, onEmptiedTrashHandler, openEmptyTrashModal, pagingResult?.totalCount]);
 
   const emptyTrashButton = useMemo(() => {
-    return <EmptyTrashButton onEmptyTrashButtonClick={emptyTrashClickHandler} disableEmptyButton={deletablePages?.length === 0} />;
-  }, [emptyTrashClickHandler, deletablePages?.length]);
+    return <EmptyTrashButton onEmptyTrashButtonClick={emptyTrashClickHandler} disableEmptyButton={deletablePages?.length === 0 || !!isReadOnlyUser} />;
+  }, [emptyTrashClickHandler, deletablePages?.length, isReadOnlyUser]);
 
   return emptyTrashButton;
 };

+ 3 - 2
apps/app/src/pages/trash.page.tsx

@@ -16,7 +16,7 @@ import { BasicLayout } from '../components/Layout/BasicLayout';
 import {
   useCurrentUser, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
-  useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL, useIsGuestUser,
+  useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL, useIsGuestUser, useIsReadOnlyUser,
 } from '../stores/context';
 
 import type { NextPageWithLayout } from './_app.page';
@@ -57,6 +57,7 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 
   const { data: isDrawerMode } = useDrawerMode();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
   const title = generateCustomTitleForPage(props, '/trash');
 
@@ -70,7 +71,7 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
           <GrowiSubNavigation
             pagePath="/trash"
             showDrawerToggler={isDrawerMode}
-            isGuestUser={isGuestUser}
+            isTagLabelsDisabled={!!isGuestUser || !!isReadOnlyUser}
             isDrawerMode={isDrawerMode}
             additionalClasses={['container-fluid']}
           />

+ 4 - 3
apps/app/src/stores/context.tsx

@@ -231,14 +231,15 @@ export const useIsReadOnlyUser = (): SWRResponse<boolean, Error> => {
 
 export const useIsEditable = (): SWRResponse<boolean, Error> => {
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isForbidden } = useIsForbidden();
   const { data: isNotCreatable } = useIsNotCreatable();
   const { data: isIdenticalPath } = useIsIdenticalPath();
 
   return useSWRImmutable(
-    ['isEditable', isGuestUser, isForbidden, isNotCreatable, isIdenticalPath],
-    ([, isGuestUser, isForbidden, isNotCreatable, isIdenticalPath]) => {
-      return (!isForbidden && !isIdenticalPath && !isNotCreatable && !isGuestUser);
+    ['isEditable', isGuestUser, isReadOnlyUser, isForbidden, isNotCreatable, isIdenticalPath],
+    ([, isGuestUser, isReadOnlyUser, isForbidden, isNotCreatable, isIdenticalPath]) => {
+      return (!isForbidden && !isIdenticalPath && !isNotCreatable && !isGuestUser && !isReadOnlyUser);
     },
   );
 };

+ 3 - 2
apps/app/src/stores/editor.tsx

@@ -10,7 +10,7 @@ import { IEditorSettings } from '~/interfaces/editor-settings';
 import { SlackChannels } from '~/interfaces/user-trigger-notification';
 
 import {
-  useCurrentUser, useDefaultIndentSize, useIsGuestUser,
+  useCurrentUser, useDefaultIndentSize, useIsGuestUser, useIsReadOnlyUser,
 } from './context';
 // import { localStorageMiddleware } from './middlewares/sync-to-storage';
 import { useSWRxTagsInfo } from './page';
@@ -37,9 +37,10 @@ type EditorSettingsOperation = {
 export const useEditorSettings = (): SWRResponseWithUtils<EditorSettingsOperation, IEditorSettings, Error> => {
   const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
   const swrResult = useSWRImmutable(
-    isGuestUser ? null : ['/personal-setting/editor-settings', currentUser?.username],
+    (isGuestUser || isReadOnlyUser) ? null : ['/personal-setting/editor-settings', currentUser?.username],
     ([endpoint]) => apiv3Get(endpoint).then(result => result.data),
     {
       // use: [localStorageMiddleware], // store to localStorage for initialization fastly

+ 5 - 2
apps/app/src/stores/page.tsx

@@ -19,7 +19,9 @@ import { IRevision, IRevisionHasId } from '~/interfaces/revision';
 
 import { IPageTagsInfo } from '../interfaces/tag';
 
-import { useCurrentPathname, useShareLinkId, useIsGuestUser } from './context';
+import {
+  useCurrentPathname, useShareLinkId, useIsGuestUser, useIsReadOnlyUser,
+} from './context';
 import { useStaticSWR } from './use-static-swr';
 
 const { isPermalink: _isPermalink } = pagePathUtils;
@@ -197,9 +199,10 @@ export const useSWRxIsGrantNormalized = (
 ): SWRResponse<IResIsGrantNormalized, Error> => {
 
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isNotFound } = useIsNotFound();
 
-  const key = !isGuestUser && !isNotFound && pageId != null
+  const key = !isGuestUser && !isReadOnlyUser && !isNotFound && pageId != null
     ? ['/page/is-grant-normalized', pageId]
     : null;
 

+ 3 - 2
apps/app/src/stores/ui.tsx

@@ -26,7 +26,7 @@ import {
 import loggerFactory from '~/utils/logger';
 
 import {
-  useIsEditable,
+  useIsEditable, useIsReadOnlyUser,
   useIsSharedUser, useIsIdenticalPath, useCurrentUser, useShareLinkId,
 } from './context';
 import { useStaticSWR } from './use-static-swr';
@@ -413,9 +413,10 @@ export const usePageTreeDescCountMap = (initialData?: UpdateDescCountData): SWRR
 
 export const useIsAbleToShowTrashPageManagementButtons = (): SWRResponse<boolean, Error> => {
   const { data: currentUser } = useCurrentUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isTrashPage } = useIsTrashPage();
 
-  return useStaticSWR('isAbleToShowTrashPageManagementButtons', isTrashPage && currentUser != null);
+  return useStaticSWR('isAbleToShowTrashPageManagementButtons', isTrashPage && currentUser != null && !isReadOnlyUser);
 };
 
 export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> => {