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

add is read only user to client side

ryoji-s 2 лет назад
Родитель
Сommit
d06abd295d

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

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

+ 7 - 5
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -17,7 +17,7 @@ import {
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
 import {
   useCurrentPathname,
   useCurrentPathname,
-  useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId, useIsContainerFluid, useIsIdenticalPath,
+  useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId, useIsContainerFluid, useIsIdenticalPath,
 } from '~/stores/context';
 } from '~/stores/context';
 import { usePageTagsForEditors } from '~/stores/editor';
 import { usePageTagsForEditors } from '~/stores/editor';
 import {
 import {
@@ -81,6 +81,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
   } = props;
   } = props;
 
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
 
 
   const { open: openPresentationModal } = usePagePresentationModal();
   const { open: openPresentationModal } = usePagePresentationModal();
@@ -117,7 +118,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
       */}
       */}
       <DropdownItem
       <DropdownItem
         onClick={() => openAccessoriesModal(PageAccessoriesModalContents.PageHistory)}
         onClick={() => openAccessoriesModal(PageAccessoriesModalContents.PageHistory)}
-        disabled={isGuestUser || isSharedUser}
+        disabled={!!isGuestUser || !!isReadOnlyUser || !!isSharedUser}
         data-testid="open-page-accessories-modal-btn-with-history-tab"
         data-testid="open-page-accessories-modal-btn-with-history-tab"
         className="grw-page-control-dropdown-item"
         className="grw-page-control-dropdown-item"
       >
       >
@@ -138,7 +139,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         {t('attachment_data')}
         {t('attachment_data')}
       </DropdownItem>
       </DropdownItem>
 
 
-      { !isGuestUser && !isSharedUser && (
+      { !isGuestUser && !isReadOnlyUser && !isSharedUser && (
         <NotAvailable isDisabled={isLinkSharingDisabled ?? false} title="Disabled by admin">
         <NotAvailable isDisabled={isLinkSharingDisabled ?? false} title="Disabled by admin">
           <DropdownItem
           <DropdownItem
             onClick={() => openAccessoriesModal(PageAccessoriesModalContents.ShareLink)}
             onClick={() => openAccessoriesModal(PageAccessoriesModalContents.ShareLink)}
@@ -212,6 +213,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
   const { data: isIdenticalPath } = useIsIdenticalPath();
   const { data: isIdenticalPath } = useIsIdenticalPath();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isContainerFluid } = useIsContainerFluid();
   const { data: isContainerFluid } = useIsContainerFluid();
 
 
@@ -384,7 +386,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
             {isAbleToChangeEditorMode && (
             {isAbleToChangeEditorMode && (
               <PageEditorModeManager
               <PageEditorModeManager
                 onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
                 onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
-                isBtnDisabled={isGuestUser}
+                isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
                 editorMode={editorMode}
                 editorMode={editorMode}
               />
               />
             )}
             )}
@@ -429,7 +431,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       pageId={currentPage?._id}
       pageId={currentPage?._id}
       showDrawerToggler={isDrawerMode}
       showDrawerToggler={isDrawerMode}
       showTagLabel={isAbleToShowTagLabel}
       showTagLabel={isAbleToShowTagLabel}
-      isGuestUser={isGuestUser}
+      isTagLabelsDisabled={!!isGuestUser || !!isReadOnlyUser}
       isDrawerMode={isDrawerMode}
       isDrawerMode={isDrawerMode}
       isCompactMode={isCompactMode}
       isCompactMode={isCompactMode}
       tags={isViewMode ? tagsInfoData?.tags : tagsForEditors}
       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 { UncontrolledTooltip } from 'reactstrap';
 
 
 import {
 import {
-  useIsSearchPage, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useIsDefaultLogo,
+  useIsSearchPage, useIsGuestUser, useIsReadOnlyUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useIsDefaultLogo,
 } from '~/stores/context';
 } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
@@ -31,6 +31,7 @@ const NavbarRight = memo((): JSX.Element => {
 
 
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
 
   // ripple
   // ripple
   const newButtonRef = useRef(null);
   const newButtonRef = useRef(null);
@@ -47,18 +48,20 @@ const NavbarRight = memo((): JSX.Element => {
           <InAppNotificationDropdown />
           <InAppNotificationDropdown />
         </li>
         </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">
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
           <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
           <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
@@ -69,7 +72,7 @@ const NavbarRight = memo((): JSX.Element => {
         </li>
         </li>
       </>
       </>
     );
     );
-  }, [t, isAuthenticated, openCreateModal, currentPagePath]);
+  }, [isReadOnlyUser, t, isAuthenticated, openCreateModal, currentPagePath]);
 
 
   const notAuthenticatedNavItem = useMemo(() => {
   const notAuthenticatedNavItem = useMemo(() => {
     return (
     return (

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

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

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

@@ -10,7 +10,7 @@ import { toastError } from '~/client/util/toastr';
 import {
 import {
   IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
   IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
-import { useIsGuestUser } from '~/stores/context';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 
 
 import { useSWRBookmarkInfo } from '../../stores/bookmark';
 import { useSWRBookmarkInfo } from '../../stores/bookmark';
@@ -90,6 +90,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   } = props;
   } = props;
 
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
 
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId, shareLinkId);
   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 seenUsers = usersList != null ? usersList.filter(({ _id }) => seenUserIds.includes(_id)).slice(0, 15) : [];
 
 
   const subscribeClickhandler = useCallback(async() => {
   const subscribeClickhandler = useCallback(async() => {
-    if (isGuestUser == null || isGuestUser) {
+    if (isGuestUser ?? true) {
       return;
       return;
     }
     }
     if (!isIPageInfoForOperation(pageInfo)) {
     if (!isIPageInfoForOperation(pageInfo)) {
@@ -116,7 +117,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
   }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
 
 
   const likeClickhandler = useCallback(async() => {
   const likeClickhandler = useCallback(async() => {
-    if (isGuestUser == null || isGuestUser) {
+    if (isGuestUser ?? true) {
       return;
       return;
     }
     }
     if (!isIPageInfoForOperation(pageInfo)) {
     if (!isIPageInfoForOperation(pageInfo)) {
@@ -128,7 +129,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
   }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
 
 
   const bookmarkClickHandler = useCallback(async() => {
   const bookmarkClickHandler = useCallback(async() => {
-    if (isGuestUser == null || isGuestUser) {
+    if (isGuestUser ?? true) {
       return;
       return;
     }
     }
     if (!isIPageInfoForOperation(pageInfo)) {
     if (!isIPageInfoForOperation(pageInfo)) {
@@ -184,7 +185,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   }, [onClickDeleteMenuItem, pageId, pageInfo, path, revisionId]);
   }, [onClickDeleteMenuItem, pageId, pageInfo, path, revisionId]);
 
 
   const switchContentWidthClickHandler = useCallback(async(newValue: boolean) => {
   const switchContentWidthClickHandler = useCallback(async(newValue: boolean) => {
-    if (onClickSwitchContentWidth == null || isGuestUser == null || isGuestUser) {
+    if (onClickSwitchContentWidth == null || (isGuestUser ?? true) || (isReadOnlyUser ?? true)) {
       return;
       return;
     }
     }
     if (!isIPageInfoForEntity(pageInfo)) {
     if (!isIPageInfoForEntity(pageInfo)) {
@@ -196,7 +197,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [isGuestUser, onClickSwitchContentWidth, pageId, pageInfo]);
+  }, [isGuestUser, isReadOnlyUser, onClickSwitchContentWidth, pageId, pageInfo]);
 
 
   const additionalMenuItemOnTopRenderer = useMemo(() => {
   const additionalMenuItemOnTopRenderer = useMemo(() => {
     if (!isIPageInfoForEntity(pageInfo)) {
     if (!isIPageInfoForEntity(pageInfo)) {
@@ -258,7 +259,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
           alignRight
           alignRight
           pageId={pageId}
           pageId={pageId}
           pageInfo={pageInfo}
           pageInfo={pageInfo}
-          isEnableActions={!isGuestUser}
+          isEnableActions={!(isGuestUser || isReadOnlyUser)}
           forceHideMenuItems={forceHideMenuItemsWithBookmark}
           forceHideMenuItems={forceHideMenuItemsWithBookmark}
           additionalMenuItemOnTopRenderer={additionalMenuItemOnTopRenderer}
           additionalMenuItemOnTopRenderer={additionalMenuItemOnTopRenderer}
           additionalMenuItemRenderer={additionalMenuItemRenderer}
           additionalMenuItemRenderer={additionalMenuItemRenderer}

+ 5 - 2
apps/app/src/components/NotAvailableForGuest.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { useIsGuestUser } from '~/stores/context';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 
 
 import { NotAvailable } from './NotAvailable';
 import { NotAvailable } from './NotAvailable';
 
 
@@ -11,11 +11,14 @@ type NotAvailableForGuestProps = {
   children: JSX.Element
   children: JSX.Element
 }
 }
 
 
+// TODO: Update NotAvailableForGuest to be used even when isReadOnlyUser
+// https://redmine.weseek.co.jp/issues/121331
 export const NotAvailableForGuest = React.memo(({ children }: NotAvailableForGuestProps): JSX.Element => {
 export const NotAvailableForGuest = React.memo(({ children }: NotAvailableForGuestProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
 
-  const isDisabled = !!isGuestUser;
+  const isDisabled = !!isGuestUser || !!isReadOnlyUser;
   const title = t('Not available for guest');
   const title = t('Not available for guest');
 
 
   return (
   return (

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

@@ -6,12 +6,12 @@ import { NotAvailableForGuest } from '../NotAvailableForGuest';
 
 
 type RenderTagLabelsProps = {
 type RenderTagLabelsProps = {
   tags: string[],
   tags: string[],
-  isGuestUser: boolean,
+  isTagLabelsDisabled: boolean,
   openEditorModal?: () => void,
   openEditorModal?: () => void,
 }
 }
 
 
 const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
 const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
-  const { tags, isGuestUser, openEditorModal } = props;
+  const { tags, isTagLabelsDisabled, openEditorModal } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   function openEditorHandler() {
   function openEditorHandler() {
@@ -35,7 +35,7 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
       <NotAvailableForGuest>
       <NotAvailableForGuest>
         <div id="edit-tags-btn-wrapper-for-tooltip">
         <div id="edit-tags-btn-wrapper-for-tooltip">
           <a
           <a
-            className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isGuestUser && 'disabled'}`}
+            className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isTagLabelsDisabled && 'disabled'}`}
             onClick={openEditorHandler}
             onClick={openEditorHandler}
           >
           >
             { isTagsEmpty && <>{ t('Add tags for this page') }</>}
             { isTagsEmpty && <>{ t('Add tags for this page') }</>}

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

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

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

@@ -6,7 +6,7 @@ import {
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import {
 import {
-  useDisableLinkSharing, useIsGuestUser, useIsSharedUser,
+  useDisableLinkSharing, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser,
 } from '~/stores/context';
 } from '~/stores/context';
 import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
 import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
 
 
@@ -34,6 +34,7 @@ const PageAccessoriesModal = (): JSX.Element => {
 
 
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isLinkSharingDisabled } = useDisableLinkSharing();
   const { data: isLinkSharingDisabled } = useDisableLinkSharing();
 
 
   const { data: status, mutate, close } = usePageAccessoriesModal();
   const { data: status, mutate, close } = usePageAccessoriesModal();
@@ -78,7 +79,7 @@ const PageAccessoriesModal = (): JSX.Element => {
           return <PageHistory onClose={close} sourceRevisionId={sourceRevisionId} targetRevisionId={targetRevisionId}/>;
           return <PageHistory onClose={close} sourceRevisionId={sourceRevisionId} targetRevisionId={targetRevisionId}/>;
         },
         },
         i18n: t('History'),
         i18n: t('History'),
-        isLinkEnabled: () => !isGuestUser && !isSharedUser,
+        isLinkEnabled: () => !isGuestUser && !isReadOnlyUser && !isSharedUser,
       },
       },
       [PageAccessoriesModalContents.Attachment]: {
       [PageAccessoriesModalContents.Attachment]: {
         Icon: AttachmentIcon,
         Icon: AttachmentIcon,
@@ -93,10 +94,10 @@ const PageAccessoriesModal = (): JSX.Element => {
           return <ShareLink />;
           return <ShareLink />;
         },
         },
         i18n: t('share_links.share_link_management'),
         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(() => (
   const buttons = useMemo(() => (
     <div className="d-flex flex-nowrap">
     <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 { IAttachmentHasId } from '@growi/core';
 
 
 import { useSWRxAttachments } from '~/stores/attachment';
 import { useSWRxAttachments } from '~/stores/attachment';
-import { useIsGuestUser } from '~/stores/context';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
 import { useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
 
 
 import { DeleteAttachmentModal } from './PageAttachment/DeleteAttachmentModal';
 import { DeleteAttachmentModal } from './PageAttachment/DeleteAttachmentModal';
@@ -25,6 +25,9 @@ const PageAttachment = (): JSX.Element => {
   // Static SWRs
   // Static SWRs
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
+
+  const isPageAttachmentDisabled = !!isGuestUser || !!isReadOnlyUser;
 
 
   // States
   // States
   const [pageNumber, setPageNumber] = useState(1);
   const [pageNumber, setPageNumber] = useState(1);
@@ -93,13 +96,13 @@ const PageAttachment = (): JSX.Element => {
         attachments={dataAttachments.attachments}
         attachments={dataAttachments.attachments}
         inUse={inUseAttachmentsMap}
         inUse={inUseAttachmentsMap}
         onAttachmentDeleteClicked={onAttachmentDeleteClicked}
         onAttachmentDeleteClicked={onAttachmentDeleteClicked}
-        isUserLoggedIn={!isGuestUser}
+        isUserLoggedIn={!isPageAttachmentDisabled}
       />
       />
     );
     );
-  }, [dataAttachments, inUseAttachmentsMap, isGuestUser, onAttachmentDeleteClicked]);
+  }, [dataAttachments, inUseAttachmentsMap, isPageAttachmentDisabled, onAttachmentDeleteClicked]);
 
 
   const renderDeleteAttachmentModal = useCallback(() => {
   const renderDeleteAttachmentModal = useCallback(() => {
-    if (isGuestUser) {
+    if (isPageAttachmentDisabled) {
       return <></>;
       return <></>;
     }
     }
 
 
@@ -120,7 +123,7 @@ const PageAttachment = (): JSX.Element => {
       />
       />
     );
     );
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  }, [attachmentToDelete, dataAttachments, deleteError, deleting, isGuestUser, onAttachmentDeleteClickedConfirmHandler, onToggleHandler]);
+  }, [attachmentToDelete, dataAttachments, deleteError, deleting, isPageAttachmentDisabled, onAttachmentDeleteClickedConfirmHandler, onToggleHandler]);
 
 
   const renderPaginationWrapper = useCallback(() => {
   const renderPaginationWrapper = useCallback(() => {
     if (dataAttachments == null || dataAttachments.attachments.length === 0) {
     if (dataAttachments == null || dataAttachments.attachments.length === 0) {

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

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

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

@@ -9,7 +9,9 @@ import {
 } from '@growi/remark-drawio';
 } from '@growi/remark-drawio';
 import { useTranslation } from 'next-i18next';
 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 '@growi/remark-drawio/dist/style.css';
 import styles from './DrawioViewerWithEditButton.module.scss';
 import styles from './DrawioViewerWithEditButton.module.scss';
@@ -27,6 +29,7 @@ export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps):
   const { bol, eol } = props;
   const { bol, eol } = props;
 
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
   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 (
   return (
     <div className={`drawio-viewer-with-edit-button ${styles['drawio-viewer-with-edit-button']}`}>
     <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 { useRouter } from 'next/router';
 import { Element } from 'react-markdown/lib/rehype-filter';
 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 loggerFactory from '~/utils/logger';
 
 
 import { NextLink } from './NextLink';
 import { NextLink } from './NextLink';
@@ -60,6 +62,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
   } = props;
   } = props;
 
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
 
 
@@ -107,7 +110,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
     };
     };
   }, [activateByHash, router.events]);
   }, [activateByHash, router.events]);
 
 
-  const showEditButton = !isGuestUser && !isSharedUser && shareLinkId == null;
+  const showEditButton = !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
 
 
   return (
   return (
     <CustomTag id={id} className={`revision-head ${styles['revision-head']} ${isActive ? 'blink' : ''}`}>
     <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 { 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';
 import styles from './TableWithEditButton.module.scss';
 
 
@@ -24,6 +26,7 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
   const { children, node, className } = props;
   const { children, node, className } = props;
 
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
 
 
@@ -34,7 +37,7 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
     globalEmitter.emit('launchHandsonTableModal', bol, eol);
     globalEmitter.emit('launchHandsonTableModal', bol, eol);
   }, [bol, eol]);
   }, [bol, eol]);
 
 
-  const showEditButton = !isGuestUser && !isSharedUser && shareLinkId == null;
+  const showEditButton = !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
 
 
   return (
   return (
     <div className={`editable-with-handsontable ${styles['editable-with-handsontable']}`}>
     <div className={`editable-with-handsontable ${styles['editable-with-handsontable']}`}>

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

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

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

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

@@ -2,7 +2,7 @@ import React, { FC, memo } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 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 { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 
 
@@ -24,6 +24,7 @@ const PageTree: FC = memo(() => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPath } = useCurrentPagePath();
   const { data: currentPath } = useCurrentPagePath();
   const { data: targetId } = useCurrentPageId();
   const { data: targetId } = useCurrentPageId();
   const { data: targetAndAncestorsData } = useTargetAndAncestors();
   const { data: targetAndAncestorsData } = useTargetAndAncestors();
@@ -57,7 +58,7 @@ const PageTree: FC = memo(() => {
   /*
   /*
    * dependencies
    * dependencies
    */
    */
-  if (isGuestUser == null) {
+  if (isGuestUser == null || isReadOnlyUser == null) {
     return null;
     return null;
   }
   }
 
 
@@ -67,13 +68,13 @@ const PageTree: FC = memo(() => {
     <div className="px-3">
     <div className="px-3">
       <PageTreeHeader />
       <PageTreeHeader />
       <ItemsTree
       <ItemsTree
-        isEnableActions={!isGuestUser}
+        isEnableActions={!(isGuestUser || isReadOnlyUser)}
         targetPath={path}
         targetPath={path}
         targetPathOrId={targetPathOrId}
         targetPathOrId={targetPathOrId}
         targetAndAncestorsData={targetAndAncestorsData}
         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="grw-pagetree-footer border-top py-3 w-100">
           <div className="private-legacy-pages-link px-3 py-2">
           <div className="private-legacy-pages-link px-3 py-2">
             <PrivateLegacyPagesLink />
             <PrivateLegacyPagesLink />

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

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