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

only publicize pages only granted to the group to be deleted

Futa Arai 2 лет назад
Родитель
Сommit
2cf6b48c71

+ 3 - 2
apps/app/public/static/locales/en_US/admin.json

@@ -834,9 +834,10 @@
       "dropdown_desc": "Choose an action for private pages",
       "select_group": "Select a group",
       "no_groups": "No groups to select",
-      "publish_pages": "Publish all",
+      "publish_pages": "Publish pages that are publishable",
       "delete_pages": "Delete all",
-      "transfer_pages": "Transfer to another group"
+      "transfer_pages": "Transfer to another group",
+      "option_explanation": "A \"publishable\" page is a page visible only to the group you want to delete. Pages that can be viewed by other groups will not be published."
     },
     "update_parent_confirm_modal": {
       "header": "The parent of the group will be changed",

+ 3 - 2
apps/app/public/static/locales/ja_JP/admin.json

@@ -844,9 +844,10 @@
       "dropdown_desc": "削除するグループの限定公開ページの処理を選択してください",
       "select_group": "グループを選択してください",
       "no_groups": "グループがありません",
-      "publish_pages": "全て公開する",
+      "publish_pages": "公開可能なページを公開する",
       "delete_pages": "全て削除する",
-      "transfer_pages": "全て他のグループに移譲する"
+      "transfer_pages": "全て他のグループに移譲する",
+      "option_explanation": "「公開可能なページ」とは、削除するグループにのみ限定公開されているページを指します。他のグループも閲覧可能なページは公開対象となりません。"
     },
     "update_parent_confirm_modal": {
       "header": "グループの親が変更されます",

+ 3 - 2
apps/app/public/static/locales/zh_CN/admin.json

@@ -843,9 +843,10 @@
       "dropdown_desc": "为私人页选择操作",
       "select_group": "选择组",
       "no_groups": "没有可选择的组",
-      "publish_pages": "全部发布",
+      "publish_pages": "发布可以发布的页面",
       "delete_pages": "全部删除",
-      "transfer_pages": "转移到另一组"
+      "transfer_pages": "转移到另一组",
+      "option_explanation": "\"可发布页面\"是指仅对您要删除的群组可见的页面。其他群组可以查看的页面将不会被发布。"
     },
     "update_parent_confirm_modal": {
       "header": "该组的父组被改变",

+ 6 - 4
apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -1,6 +1,5 @@
-import React, {
-  FC, useCallback, useState, useMemo,
-} from 'react';
+import type { FC } from 'react';
+import React, { useCallback, useState, useMemo } from 'react';
 
 import type { IUserGroupHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
@@ -196,7 +195,7 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
       </ModalBody>
       <ModalFooter>
         <form className="d-flex justify-content-between w-100" onSubmit={handleSubmit}>
-          <div className="d-flex mb-0">
+          <div className="d-flex mb-0 me-3          ">
             {renderPageActionSelector()}
             {renderGroupSelector()}
           </div>
@@ -204,6 +203,9 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
             <span className="material-symbols-outlined">delete_forever</span> {t('Delete')}
           </button>
         </form>
+        <div className="form-text text-muted">
+          <small>{t('admin:user_group_management.delete_modal.option_explanation')}</small>
+        </div>
       </ModalFooter>
     </Modal>
   );

+ 2 - 1
apps/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useState, useCallback } from 'react';
 
 import type { IUserGroup, IUserGroupHasId } from '@growi/core';
 import dynamic from 'next/dynamic';

+ 3 - 2
apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -13,8 +13,9 @@ import {
   apiv3Get, apiv3Put, apiv3Delete, apiv3Post,
 } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
-import { SearchTypes, SearchType } from '~/interfaces/user-group';
+import type { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
+import type { SearchType } from '~/interfaces/user-group';
+import { SearchTypes } from '~/interfaces/user-group';
 import Xss from '~/services/xss';
 import { useIsAclEnabled } from '~/stores/context';
 import { useUpdateUserGroupConfirmModal } from '~/stores/modal';

+ 3 - 4
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement.tsx

@@ -1,6 +1,5 @@
-import {
-  FC, useCallback, useMemo, useState,
-} from 'react';
+import type { FC } from 'react';
+import { useCallback, useMemo, useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
 import { TabContent, TabPane } from 'reactstrap';
@@ -11,7 +10,7 @@ import { UserGroupDeleteModal } from '~/components/Admin/UserGroup/UserGroupDele
 import { UserGroupModal } from '~/components/Admin/UserGroup/UserGroupModal';
 import { UserGroupTable } from '~/components/Admin/UserGroup/UserGroupTable';
 import CustomNav from '~/components/CustomNavigation/CustomNav';
-import { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
+import type { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
 import { useIsAclEnabled } from '~/stores/context';
 
 import { useSWRxChildExternalUserGroupList, useSWRxExternalUserGroupList, useSWRxExternalUserGroupRelationList } from '../../stores/external-user-group';

+ 6 - 3
apps/app/src/features/external-user-group/server/models/external-user-group.ts

@@ -1,7 +1,8 @@
-import { Schema, Model, Document } from 'mongoose';
+import type { Model, Document } from 'mongoose';
+import { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
-import { IExternalUserGroup } from '~/features/external-user-group/interfaces/external-user-group';
+import type { IExternalUserGroup } from '~/features/external-user-group/interfaces/external-user-group';
 import UserGroup from '~/server/models/user-group';
 import { getOrCreateModel } from '~/server/util/mongoose-utils';
 
@@ -12,7 +13,9 @@ export interface ExternalUserGroupModel extends Model<ExternalUserGroupDocument>
 
   PAGE_ITEMS: 10,
 
-  findGroupsWithDescendantsRecursively: (groups, descendants?) => any,
+  findGroupsWithDescendantsRecursively: (
+    groups: ExternalUserGroupDocument[], descendants?: ExternalUserGroupDocument[]
+  ) => Promise<ExternalUserGroupDocument[]>,
 }
 
 const schema = new Schema<ExternalUserGroupDocument, ExternalUserGroupModel>({

+ 5 - 4
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts

@@ -1,6 +1,7 @@
 import { GroupType } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
-import { Router, Request } from 'express';
+import type { Request } from 'express';
+import { Router } from 'express';
 import {
   body, param, query, validationResult,
 } from 'express-validator';
@@ -8,13 +9,13 @@ import {
 import ExternalUserGroup from '~/features/external-user-group/server/models/external-user-group';
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import { SupportedAction } from '~/interfaces/activity';
-import Crowi from '~/server/crowi';
+import type Crowi from '~/server/crowi';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { serializeUserGroupRelationSecurely } from '~/server/models/serializers/user-group-relation-serializer';
-import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
+import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import { configManager } from '~/server/service/config-manager';
-import UserGroupService from '~/server/service/user-group';
+import type UserGroupService from '~/server/service/user-group';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:external-user-group');

+ 2 - 0
apps/app/src/server/models/page.ts

@@ -1052,6 +1052,8 @@ schema.methods.calculateAndUpdateLatestRevisionBodyLength = async function(this:
   await this.save();
 };
 
+schema.methods.removeGroupsFromAllPageGrantedGroups = async function(pages: PageDocument[], groupIds: ObjectIdLike[]): Promise<void> {};
+
 /*
  * Merge obsolete page model methods and define new methods which depend on crowi instance
  */

+ 6 - 3
apps/app/src/server/models/user-group.ts

@@ -1,5 +1,6 @@
 import type { IUserGroup } from '@growi/core';
-import { Schema, Model, Document } from 'mongoose';
+import type { Model, Document } from 'mongoose';
+import { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
@@ -12,7 +13,7 @@ export interface UserGroupModel extends Model<UserGroupDocument> {
 
   PAGE_ITEMS: 10,
 
-  findGroupsWithDescendantsRecursively: (groups, descendants?) => any,
+  findGroupsWithDescendantsRecursively: (groups: UserGroupDocument[], descendants?: UserGroupDocument[]) => Promise<UserGroupDocument[]>,
 }
 
 /*
@@ -109,7 +110,9 @@ schema.statics.findGroupsWithAncestorsRecursively = async function(group, ancest
  * @param descendants UserGroupDocument[]
  * @returns UserGroupDocument[]
  */
-schema.statics.findGroupsWithDescendantsRecursively = async function(groups, descendants = groups) {
+schema.statics.findGroupsWithDescendantsRecursively = async function(
+    groups: UserGroupDocument[], descendants: UserGroupDocument[] = groups,
+): Promise<UserGroupDocument[]> {
   const nextGroups = await this.find({ parent: { $in: groups.map(g => g._id) } });
 
   if (nextGroups.length === 0) {

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

@@ -18,6 +18,7 @@ import mongoose from 'mongoose';
 import streamToPromise from 'stream-to-promise';
 
 import { Comment } from '~/features/comment/server';
+import type { ExternalUserGroupDocument } from '~/features/external-user-group/server/models/external-user-group';
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import { SupportedAction } from '~/interfaces/activity';
 import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
@@ -37,6 +38,7 @@ import {
 } from '~/server/models/page';
 import type { PageTagRelationDocument } from '~/server/models/page-tag-relation';
 import PageTagRelation from '~/server/models/page-tag-relation';
+import type { UserGroupDocument } from '~/server/models/user-group';
 import { createBatchStream } from '~/server/util/batch-stream';
 import loggerFactory from '~/utils/logger';
 import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
@@ -2501,20 +2503,36 @@ class PageService implements IPageService {
   }
 
 
-  async handlePrivatePagesForGroupsToDelete(groupsToDelete, action, transferToUserGroup: IGrantedGroup, user) {
-    const Page = this.crowi.model('Page');
-    const pages = await Page.find({
-      grantedGroups: {
-        $elemMatch: {
-          item: { $in: groupsToDelete },
-        },
-      },
-    });
+  async handlePrivatePagesForGroupsToDelete(
+      groupsToDelete: UserGroupDocument[] | ExternalUserGroupDocument[], action: string, transferToUserGroup: IGrantedGroup, user,
+  ): Promise<void> {
+    const Page = mongoose.model<IPage, PageModel>('Page');
+    const pages = await Page.find({ grantedGroups: { $elemMatch: { item: { $in: groupsToDelete } } } });
 
     switch (action) {
-      case 'public':
-        await Page.publicizePages(pages);
+      case 'public': {
+        const groupsToDeleteIds = groupsToDelete.map(group => group._id.toString());
+        const pageGroups = pages.reduce((acc: { canPublicize: PageDocument[], cannotPublicize: PageDocument[] }, page) => {
+          const canPublicize = page.grantedGroups.every(group => groupsToDeleteIds.includes(getIdForRef(group.item)));
+          acc[canPublicize ? 'canPublicize' : 'cannotPublicize'].push(page);
+          return acc;
+        }, { canPublicize: [], cannotPublicize: [] });
+
+        // Only publicize pages that can only be accessed by the groups to be deleted
+        await Page.publicizePages(pageGroups.canPublicize);
+        // Remove the groups to be deleted from the grantedGroups of the pages that can be accessed by other groups
+        const queries = pageGroups.cannotPublicize.map((page) => {
+          return {
+            updateOne: {
+              filter: { _id: page._id },
+              update: { $set: { grantedGroups: page.grantedGroups.filter(group => !groupsToDeleteIds.includes(getIdForRef(group.item))) } },
+            },
+          };
+        });
+        await Page.bulkWrite(queries);
+
         break;
+      }
       case 'delete':
         return this.deleteMultipleCompletely(pages, user);
       case 'transfer':

+ 8 - 6
apps/app/src/server/service/user-group.ts

@@ -1,13 +1,15 @@
 import type { IUser, IGrantedGroup } from '@growi/core';
-import { DeleteResult } from 'mongodb';
-import { Model } from 'mongoose';
+import type { DeleteResult } from 'mongodb';
+import type { Model } from 'mongoose';
 
-import { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-import UserGroup, { UserGroupDocument, UserGroupModel } from '~/server/models/user-group';
+import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
+import type { UserGroupDocument, UserGroupModel } from '~/server/models/user-group';
+import UserGroup from '~/server/models/user-group';
 import { excludeTestIdsFromTargetIds, includesObjectIds } from '~/server/util/compare-objectId';
 import loggerFactory from '~/utils/logger';
 
-import UserGroupRelation, { UserGroupRelationDocument, UserGroupRelationModel } from '../models/user-group-relation';
+import type { UserGroupRelationDocument, UserGroupRelationModel } from '../models/user-group-relation';
+import UserGroupRelation from '../models/user-group-relation';
 
 
 const logger = loggerFactory('growi:service:UserGroupService'); // eslint-disable-line no-unused-vars
@@ -144,7 +146,7 @@ class UserGroupService {
       User.findUserByUsername(username),
     ]);
 
-    const groupsOfRelationsToDelete = await UserGroup.findGroupsWithDescendantsRecursively([userGroup]);
+    const groupsOfRelationsToDelete = userGroup != null ? await UserGroup.findGroupsWithDescendantsRecursively([userGroup]) : [];
     const relatedGroupIdsToDelete = groupsOfRelationsToDelete.map(g => g._id);
 
     const deleteManyRes = await UserGroupRelation.deleteMany({ relatedUser: user._id, relatedGroup: { $in: relatedGroupIdsToDelete } });