2
0
Эх сурвалжийг харах

keep groups unrelated to user in group grant change

Futa Arai 2 жил өмнө
parent
commit
4fe196fa6c

+ 2 - 2
apps/app/_obsolete/src/components/PageEditorByHackmd.tsx

@@ -98,7 +98,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
     if (grantData == null) {
     if (grantData == null) {
       return;
       return;
     }
     }
-    const grantedGroups = grantData.grantedGroups?.map((group) => {
+    const userRelatedGrantedGroups = grantData.userRelatedGrantedGroups?.map((group) => {
       return { item: group.id, type: group.type };
       return { item: group.id, type: group.type };
     });
     });
     const optionsToSave = {
     const optionsToSave = {
@@ -106,7 +106,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       slackChannels: '', // set in save method by opts in SavePageControlls.tsx
       slackChannels: '', // set in save method by opts in SavePageControlls.tsx
       grant: grantData.grant,
       grant: grantData.grant,
       pageTags: pageTags ?? [],
       pageTags: pageTags ?? [],
-      grantUserGroupIds: grantedGroups,
+      userRelatedGrantUserGroupIds: userRelatedGrantedGroups,
     };
     };
     return optionsToSave;
     return optionsToSave;
   }, [grantData, isSlackEnabled, pageTags]);
   }, [grantData, isSlackEnabled, pageTags]);

+ 3 - 3
apps/app/src/components/PageAlert/FixPageGrantAlert.tsx

@@ -68,7 +68,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
     try {
     try {
       await apiv3Put(`/page/${pageId}/grant`, {
       await apiv3Put(`/page/${pageId}/grant`, {
         grant: selectedGrant,
         grant: selectedGrant,
-        grantedGroups: selectedGroups.length !== 0 ? selectedGroups.map((g) => {
+        userRelatedGrantedGroups: selectedGroups.length !== 0 ? selectedGroups.map((g) => {
           return { item: g.item._id, type: g.type };
           return { item: g.item._id, type: g.type };
         }) : null,
         }) : null,
       });
       });
@@ -99,10 +99,10 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
     }
     }
 
 
     if (grantData.grant === 5) {
     if (grantData.grant === 5) {
-      if (grantData.grantedGroups == null || grantData.grantedGroups.length === 0) {
+      if (grantData.userRelatedGrantedGroups == null || grantData.userRelatedGrantedGroups.length === 0) {
         return t('fix_page_grant.modal.grant_label.isForbidden');
         return t('fix_page_grant.modal.grant_label.isForbidden');
       }
       }
-      return `${t('fix_page_grant.modal.radio_btn.grant_group')} (${grantData.grantedGroups.map(g => g.name).join(', ')})`;
+      return `${t('fix_page_grant.modal.radio_btn.grant_group')} (${grantData.userRelatedGrantedGroups.map(g => g.name).join(', ')})`;
     }
     }
 
 
     throw Error('cannot get grant label'); // this error can't be throwed
     throw Error('cannot get grant label'); // this error can't be throwed

+ 2 - 2
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -217,7 +217,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     if (grantData == null) {
     if (grantData == null) {
       return;
       return;
     }
     }
-    const grantedGroups = grantData.grantedGroups?.map((group) => {
+    const userRelatedGrantedGroups = grantData.userRelatedGrantedGroups?.map((group) => {
       return { item: group.id, type: group.type };
       return { item: group.id, type: group.type };
     });
     });
     const optionsToSave = {
     const optionsToSave = {
@@ -225,7 +225,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       slackChannels: '', // set in save method by opts in SavePageControlls.tsx
       slackChannels: '', // set in save method by opts in SavePageControlls.tsx
       grant: grantData.grant,
       grant: grantData.grant,
       // pageTags: pageTags ?? [],
       // pageTags: pageTags ?? [],
-      grantUserGroupIds: grantedGroups,
+      userRelatedGrantUserGroupIds: userRelatedGrantedGroups,
     };
     };
     return optionsToSave;
     return optionsToSave;
   }, [grantData, isSlackEnabled]);
   }, [grantData, isSlackEnabled]);

+ 2 - 2
apps/app/src/components/SavePageControls.tsx

@@ -67,7 +67,7 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
     return null;
     return null;
   }
   }
 
 
-  const { grant, grantedGroups } = grantData;
+  const { grant, userRelatedGrantedGroups } = grantData;
 
 
   const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
   const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
   const labelSubmitButton = t('Update');
   const labelSubmitButton = t('Update');
@@ -82,7 +82,7 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
             <GrantSelector
             <GrantSelector
               grant={grant}
               grant={grant}
               disabled={isGrantSelectorDisabledPage}
               disabled={isGrantSelectorDisabledPage}
-              grantedGroups={grantedGroups}
+              userRelatedGrantedGroups={userRelatedGrantedGroups}
               onUpdateGrant={updateGrantHandler}
               onUpdateGrant={updateGrantHandler}
             />
             />
           </div>
           </div>

+ 18 - 18
apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx

@@ -34,7 +34,7 @@ const AVAILABLE_GRANTS = [
 type Props = {
 type Props = {
   disabled?: boolean,
   disabled?: boolean,
   grant: number,
   grant: number,
-  grantedGroups?: {
+  userRelatedGrantedGroups?: {
     id: string,
     id: string,
     name: string,
     name: string,
     type: GroupType,
     type: GroupType,
@@ -51,7 +51,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
 
 
   const {
   const {
     disabled,
     disabled,
-    grantedGroups,
+    userRelatedGrantedGroups,
     onUpdateGrant,
     onUpdateGrant,
     grant: currentGrant,
     grant: currentGrant,
   } = props;
   } = props;
@@ -80,23 +80,23 @@ export const GrantSelector = (props: Props): JSX.Element => {
     }
     }
 
 
     if (onUpdateGrant != null) {
     if (onUpdateGrant != null) {
-      onUpdateGrant({ grant, grantedGroups: undefined });
+      onUpdateGrant({ grant, userRelatedGrantedGroups: undefined });
     }
     }
   }, [onUpdateGrant, showSelectGroupModal]);
   }, [onUpdateGrant, showSelectGroupModal]);
 
 
   const groupListItemClickHandler = useCallback((grantGroup: IGrantedGroup) => {
   const groupListItemClickHandler = useCallback((grantGroup: IGrantedGroup) => {
     if (onUpdateGrant != null && isPopulated(grantGroup.item)) {
     if (onUpdateGrant != null && isPopulated(grantGroup.item)) {
-      let grantedGroupsCopy = grantedGroups != null ? [...grantedGroups] : [];
+      let userRelatedGrantedGroupsCopy = userRelatedGrantedGroups != null ? [...userRelatedGrantedGroups] : [];
       const grantGroupInfo = { id: grantGroup.item._id, name: grantGroup.item.name, type: grantGroup.type };
       const grantGroupInfo = { id: grantGroup.item._id, name: grantGroup.item.name, type: grantGroup.type };
-      if (grantedGroupsCopy.find(group => group.id === grantGroupInfo.id) == null) {
-        grantedGroupsCopy.push(grantGroupInfo);
+      if (userRelatedGrantedGroupsCopy.find(group => group.id === grantGroupInfo.id) == null) {
+        userRelatedGrantedGroupsCopy.push(grantGroupInfo);
       }
       }
       else {
       else {
-        grantedGroupsCopy = grantedGroupsCopy.filter(group => group.id !== grantGroupInfo.id);
+        userRelatedGrantedGroupsCopy = userRelatedGrantedGroupsCopy.filter(group => group.id !== grantGroupInfo.id);
       }
       }
-      onUpdateGrant({ grant: 5, grantedGroups: grantedGroupsCopy });
+      onUpdateGrant({ grant: 5, userRelatedGrantedGroups: userRelatedGrantedGroupsCopy });
     }
     }
-  }, [onUpdateGrant, grantedGroups]);
+  }, [onUpdateGrant, userRelatedGrantedGroups]);
 
 
   /**
   /**
    * Render grant selector DOM.
    * Render grant selector DOM.
@@ -107,7 +107,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
     let dropdownToggleLabelElm;
     let dropdownToggleLabelElm;
 
 
     const dropdownMenuElems = AVAILABLE_GRANTS.map((opt) => {
     const dropdownMenuElems = AVAILABLE_GRANTS.map((opt) => {
-      const label = ((opt.grant === 5 && opt.reselectLabel != null) && grantedGroups != null && grantedGroups.length > 0)
+      const label = ((opt.grant === 5 && opt.reselectLabel != null) && userRelatedGrantedGroups != null && userRelatedGrantedGroups.length > 0)
         ? opt.reselectLabel // when grantGroup is selected
         ? opt.reselectLabel // when grantGroup is selected
         : opt.label;
         : opt.label;
 
 
@@ -128,18 +128,18 @@ export const GrantSelector = (props: Props): JSX.Element => {
     });
     });
 
 
     // add specified group option
     // add specified group option
-    if (grantedGroups != null && grantedGroups.length > 0) {
+    if (userRelatedGrantedGroups != null && userRelatedGrantedGroups.length > 0) {
       const labelElm = (
       const labelElm = (
         <span>
         <span>
           <i className="icon icon-fw icon-organization"></i>
           <i className="icon icon-fw icon-organization"></i>
           <span className="label">
           <span className="label">
-            {grantedGroups.length > 1
+            {userRelatedGrantedGroups.length > 1
               ? (
               ? (
                 <span>
                 <span>
-                  {`${grantedGroups[0].name}... `}
-                  <span className="badge badge-purple">+{grantedGroups.length - 1}</span>
+                  {`${userRelatedGrantedGroups[0].name}... `}
+                  <span className="badge badge-purple">+{userRelatedGrantedGroups.length - 1}</span>
                 </span>
                 </span>
-              ) : grantedGroups[0].name}
+              ) : userRelatedGrantedGroups[0].name}
           </span>
           </span>
         </span>
         </span>
       );
       );
@@ -162,7 +162,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
         </UncontrolledDropdown>
         </UncontrolledDropdown>
       </div>
       </div>
     );
     );
-  }, [changeGrantHandler, currentGrant, disabled, grantedGroups, t]);
+  }, [changeGrantHandler, currentGrant, disabled, userRelatedGrantedGroups, t]);
 
 
   /**
   /**
    * Render select grantgroup modal.
    * Render select grantgroup modal.
@@ -195,7 +195,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
     return (
     return (
       <>
       <>
         { myUserGroups.map((group) => {
         { myUserGroups.map((group) => {
-          const groupIsGranted = grantedGroups?.find(g => g.id === group.item._id) != null;
+          const groupIsGranted = userRelatedGrantedGroups?.find(g => g.id === group.item._id) != null;
           const activeClass = groupIsGranted ? 'active' : '';
           const activeClass = groupIsGranted ? 'active' : '';
 
 
           return (
           return (
@@ -216,7 +216,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
       </>
       </>
     );
     );
 
 
-  }, [currentUser?.admin, groupListItemClickHandler, myUserGroups, shouldFetch, t, grantedGroups]);
+  }, [currentUser?.admin, groupListItemClickHandler, myUserGroups, shouldFetch, t, userRelatedGrantedGroups]);
 
 
   return (
   return (
     <>
     <>

+ 1 - 1
apps/app/src/interfaces/page-operation.ts

@@ -33,6 +33,6 @@ export type OptionsToSave = {
   isSlackEnabled: boolean;
   isSlackEnabled: boolean;
   slackChannels: string;
   slackChannels: string;
   grant: number;
   grant: number;
-  // grantUserGroupIds?: IGrantedGroup[];
+  // userRelatedGrantUserGroupIds?: IGrantedGroup[];
   // isSyncRevisionToHackmd?: boolean;
   // isSyncRevisionToHackmd?: boolean;
 };
 };

+ 1 - 1
apps/app/src/interfaces/page.ts

@@ -10,7 +10,7 @@ export type IPageForItem = Partial<IPageHasId & {isTarget?: boolean, processData
 
 
 export type IPageGrantData = {
 export type IPageGrantData = {
   grant: number,
   grant: number,
-  grantedGroups?: {
+  userRelatedGrantedGroups?: {
     id: string,
     id: string,
     name: string,
     name: string,
     type: GroupType,
     type: GroupType,

+ 10 - 6
apps/app/src/pages/[[...path]].page.tsx

@@ -3,8 +3,9 @@ import React, { ReactNode, useEffect } from 'react';
 
 
 import EventEmitter from 'events';
 import EventEmitter from 'events';
 
 
-import { isIPageInfoForEntity } from '@growi/core';
+import { isIPageInfoForEntity, isPopulated } from '@growi/core';
 import type {
 import type {
+  GroupType,
   IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision,
   IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision,
 } from '@growi/core';
 } from '@growi/core';
 import {
 import {
@@ -460,16 +461,19 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
     // apply parent page grant, without groups that user isn't related to
     // apply parent page grant, without groups that user isn't related to
     const ancestor = await Page.findAncestorByPathAndViewer(currentPathname, user);
     const ancestor = await Page.findAncestorByPathAndViewer(currentPathname, user);
     if (ancestor != null) {
     if (ancestor != null) {
-      const userRelatedGrantedGroups = await pageService.getUserRelatedGrantedGroups(ancestor, user);
-      props.grantData = {
-        grant: ancestor.grant,
-        grantedGroups: userRelatedGrantedGroups.map((group) => {
+      const userRelatedGrantedGroups = (await pageService.getUserRelatedGrantedGroups(ancestor, user)).map((group) => {
+        if (isPopulated(group.item)) {
           return {
           return {
             id: group.item._id,
             id: group.item._id,
             name: group.item.name,
             name: group.item.name,
             type: group.type,
             type: group.type,
           };
           };
-        }),
+        }
+        return null;
+      }).filter((info): info is NonNullable<{id: string, name: string, type: GroupType}> => info != null);
+      props.grantData = {
+        grant: ancestor.grant,
+        userRelatedGrantedGroups,
       };
       };
     }
     }
   }
   }

+ 1 - 1
apps/app/src/server/models/interfaces/page-operation.ts

@@ -23,7 +23,7 @@ export type IUserForResuming = {
 
 
 export type IOptionsForUpdate = {
 export type IOptionsForUpdate = {
   grant?: PageGrant,
   grant?: PageGrant,
-  grantUserGroupIds?: IGrantedGroup[],
+  userRelatedGrantUserGroupIds?: IGrantedGroup[],
   isSyncRevisionToHackmd?: boolean,
   isSyncRevisionToHackmd?: boolean,
   overwriteScopesOfDescendants?: boolean,
   overwriteScopesOfDescendants?: boolean,
 };
 };

+ 3 - 3
apps/app/src/server/routes/apiv3/page.js

@@ -565,10 +565,10 @@ module.exports = (crowi) => {
 
 
   router.put('/:pageId/grant', loginRequiredStrictly, excludeReadOnlyUser, validator.updateGrant, apiV3FormValidator, async(req, res) => {
   router.put('/:pageId/grant', loginRequiredStrictly, excludeReadOnlyUser, validator.updateGrant, apiV3FormValidator, async(req, res) => {
     const { pageId } = req.params;
     const { pageId } = req.params;
-    const { grant, grantedGroups } = req.body;
+    const { grant, userRelatedGrantedGroups } = req.body;
 
 
     // TODO: remove in https://redmine.weseek.co.jp/issues/136137
     // TODO: remove in https://redmine.weseek.co.jp/issues/136137
-    if (grantedGroups != null && grantedGroups.length > 1) {
+    if (userRelatedGrantedGroups != null && userRelatedGrantedGroups.length > 1) {
       return res.apiv3Err('Cannot grant multiple groups to page at the moment');
       return res.apiv3Err('Cannot grant multiple groups to page at the moment');
     }
     }
 
 
@@ -584,7 +584,7 @@ module.exports = (crowi) => {
     let data;
     let data;
     try {
     try {
       const shouldUseV4Process = false;
       const shouldUseV4Process = false;
-      const grantData = { grant, grantedGroups };
+      const grantData = { grant, userRelatedGrantedGroups };
       data = await crowi.pageService.updateGrant(page, req.user, grantData, shouldUseV4Process);
       data = await crowi.pageService.updateGrant(page, req.user, grantData, shouldUseV4Process);
     }
     }
     catch (err) {
     catch (err) {

+ 0 - 1
apps/app/src/server/routes/apiv3/pages.js

@@ -306,7 +306,6 @@ module.exports = (crowi) => {
    */
    */
   router.post('/', accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, addActivity, validator.createPage, apiV3FormValidator, async(req, res) => {
   router.post('/', accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, addActivity, validator.createPage, apiV3FormValidator, async(req, res) => {
     const {
     const {
-      // body, grant, grantUserGroupId, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, pageTags, shouldGeneratePath,
       body, grant, grantUserGroupIds, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, pageTags, shouldGeneratePath,
       body, grant, grantUserGroupIds, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, pageTags, shouldGeneratePath,
     } = req.body;
     } = req.body;
 
 

+ 41 - 15
apps/app/src/server/service/page.ts

@@ -5,7 +5,9 @@ import type {
   Ref, HasObjectId, IUserHasId, IUser,
   Ref, HasObjectId, IUserHasId, IUser,
   IPage, IPageInfo, IPageInfoAll, IPageInfoForEntity, IPageWithMeta, IGrantedGroup,
   IPage, IPageInfo, IPageInfoAll, IPageInfoForEntity, IPageWithMeta, IGrantedGroup,
 } from '@growi/core';
 } from '@growi/core';
-import { PageGrant, PageStatus, getIdForRef } from '@growi/core';
+import {
+  PageGrant, PageStatus, getIdForRef, isPopulated,
+} from '@growi/core';
 import {
 import {
   pagePathUtils, pathUtils,
   pagePathUtils, pathUtils,
 } from '@growi/core/dist/utils';
 } from '@growi/core/dist/utils';
@@ -2360,15 +2362,19 @@ class PageService {
   }
   }
 
 
   /*
   /*
- * get all groups of Page that user is related to
- */
-  async getUserRelatedGrantedGroups(page: PageDocument, user): Promise<PopulatedGrantedGroup[]> {
-    const populatedPage = await page.populate<{grantedGroups: PopulatedGrantedGroup[] | null}>('grantedGroups.item');
-    const userRelatedGroupIds = [
+  * get all groups of Page that user is related to
+  */
+  async getUserRelatedGrantedGroups(page: PageDocument, user): Promise<IGrantedGroup[]> {
+    const userRelatedGroupIds: string[] = [
       ...(await UserGroupRelation.findAllGroupsForUser(user)).map(ugr => ugr._id.toString()),
       ...(await UserGroupRelation.findAllGroupsForUser(user)).map(ugr => ugr._id.toString()),
       ...(await ExternalUserGroupRelation.findAllGroupsForUser(user)).map(eugr => eugr._id.toString()),
       ...(await ExternalUserGroupRelation.findAllGroupsForUser(user)).map(eugr => eugr._id.toString()),
     ];
     ];
-    return populatedPage.grantedGroups?.filter(group => userRelatedGroupIds.includes(group.item._id.toString())) || [];
+    return page.grantedGroups?.filter((group) => {
+      if (isPopulated(group.item)) {
+        return userRelatedGroupIds.includes(group.item._id.toString());
+      }
+      return userRelatedGroupIds.includes(group.item);
+    }) || [];
   }
   }
 
 
   private async revertDeletedPageV4(page, user, options = {}, isRecursively = false) {
   private async revertDeletedPageV4(page, user, options = {}, isRecursively = false) {
@@ -4004,12 +4010,12 @@ class PageService {
    * @param {UserDocument} user
    * @param {UserDocument} user
    * @param options
    * @param options
    */
    */
-  async updateGrant(page, user, grantData: {grant: PageGrant, grantedGroups: IGrantedGroup[]}): Promise<PageDocument> {
-    const { grant, grantedGroups } = grantData;
+  async updateGrant(page, user, grantData: {grant: PageGrant, userRelatedGrantedGroups: IGrantedGroup[]}): Promise<PageDocument> {
+    const { grant, userRelatedGrantedGroups } = grantData;
 
 
     const options: IOptionsForUpdate = {
     const options: IOptionsForUpdate = {
       grant,
       grant,
-      grantUserGroupIds: grantedGroups,
+      userRelatedGrantUserGroupIds: userRelatedGrantedGroups,
       isSyncRevisionToHackmd: false,
       isSyncRevisionToHackmd: false,
     };
     };
 
 
@@ -4055,6 +4061,21 @@ class PageService {
     await PageOperation.findByIdAndDelete(pageOpId);
     await PageOperation.findByIdAndDelete(pageOpId);
   }
   }
 
 
+  /**
+   * Get the new GrantedGroups for the page going through an update operation.
+   * It will include the groups specified by the operator, and groups which the user does not belong to, but was related to the page before the update.
+   * @param userRelatedGrantedGroups The groups specified by the operator
+   * @param page The page going through an update operation
+   * @param user The operator
+   * @returns The new GrantedGroups array to be set to the page
+   */
+  async getNewGrantedGroups(userRelatedGrantedGroups: IGrantedGroup[], page: PageDocument, user): Promise<IGrantedGroup[]> {
+    const previousGrantedGroups = page.grantedGroups;
+    const userRelatedPreviousGrantedGroups = (await this.getUserRelatedGrantedGroups(page, user)).map(g => getIdForRef(g.item));
+    const userUnrelatedPreviousGrantedGroups = previousGrantedGroups.filter(g => !userRelatedPreviousGrantedGroups.includes(getIdForRef(g.item)));
+    return [...userUnrelatedPreviousGrantedGroups, ...userRelatedGrantedGroups];
+  }
+
   async updatePage(
   async updatePage(
       pageData: PageDocument,
       pageData: PageDocument,
       body: string | null,
       body: string | null,
@@ -4078,8 +4099,11 @@ class PageService {
     const clonedPageData = Page.hydrate(pageData.toObject());
     const clonedPageData = Page.hydrate(pageData.toObject());
     const newPageData = pageData;
     const newPageData = pageData;
 
 
-    const grant = options.grant ?? clonedPageData.grant; // use the previous data if absence
-    const grantUserGroupIds = options.grantUserGroupIds ?? clonedPageData.grantedGroups;
+    // use the previous data if absence
+    const grant = options.grant ?? clonedPageData.grant;
+    const grantUserGroupIds = options.userRelatedGrantUserGroupIds != null
+      ? (await this.getNewGrantedGroups(options.userRelatedGrantUserGroupIds, clonedPageData, user))
+      : clonedPageData.grantedGroups;
 
 
     const grantedUserIds = clonedPageData.grantedUserIds || [user._id];
     const grantedUserIds = clonedPageData.grantedUserIds || [user._id];
     const shouldBeOnTree = grant !== PageGrant.GRANT_RESTRICTED;
     const shouldBeOnTree = grant !== PageGrant.GRANT_RESTRICTED;
@@ -4101,7 +4125,7 @@ class PageService {
       }
       }
 
 
       if (options.overwriteScopesOfDescendants) {
       if (options.overwriteScopesOfDescendants) {
-        const updateGrantInfo = await this.pageGrantService.generateUpdateGrantInfoToOverwriteDescendants(user, grant, options.grantUserGroupIds);
+        const updateGrantInfo = await this.pageGrantService.generateUpdateGrantInfoToOverwriteDescendants(user, grant, options.userRelatedGrantUserGroupIds);
         const canOverwriteDescendants = await this.pageGrantService.canOverwriteDescendants(clonedPageData.path, user, updateGrantInfo);
         const canOverwriteDescendants = await this.pageGrantService.canOverwriteDescendants(clonedPageData.path, user, updateGrantInfo);
 
 
         if (!canOverwriteDescendants) {
         if (!canOverwriteDescendants) {
@@ -4191,10 +4215,12 @@ class PageService {
     const Revision = mongoose.model('Revision') as any; // TODO: TypeScriptize model
     const Revision = mongoose.model('Revision') as any; // TODO: TypeScriptize model
 
 
     const grant = options.grant || pageData.grant; // use the previous data if absence
     const grant = options.grant || pageData.grant; // use the previous data if absence
-    const grantUserGroupIds = options.grantUserGroupIds || pageData.grantUserGroupIds; // use the previous data if absence
+    const grantUserGroupIds = options.userRelatedGrantUserGroupIds != null
+      ? (await this.getNewGrantedGroups(options.userRelatedGrantUserGroupIds, pageData, user))
+      : pageData.grantedGroups;
     const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
     const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
 
 
-    // TODO 136137: validate multiple group grant before save using pageData and options
+    // validate multiple group grant before save using pageData and options
     await this.pageGrantService.validateGrantChange(user, pageData.grantedGroups, grant, grantUserGroupIds);
     await this.pageGrantService.validateGrantChange(user, pageData.grantedGroups, grant, grantUserGroupIds);
 
 
     await this.validateAppliedScope(user, grant, grantUserGroupIds);
     await this.validateAppliedScope(user, grant, grantUserGroupIds);