diff --git a/app/controllers/system/admin/users_controller.rb b/app/controllers/system/admin/users_controller.rb index c079d062d1a..4babcf33bf9 100644 --- a/app/controllers/system/admin/users_controller.rb +++ b/app/controllers/system/admin/users_controller.rb @@ -7,7 +7,9 @@ def index format.json do load_users load_counts - @instances_preload_service = User::InstancePreloadService.new(@users.map(&:id)) + user_ids = @users.map(&:id) + @instances_preload_service = User::InstancePreloadService.new(user_ids) + @user_course_hash = get_user_course_hash(user_ids) end end end @@ -16,7 +18,7 @@ def update @instances_preload_service = User::InstancePreloadService.new(@user.id) if @user.update(user_params) render 'system/admin/users/_user_list_data', - locals: { user: @user }, + locals: { user: @user, course_users: get_user_course_hash([@user.id]).fetch(@user.id, []) }, status: :ok else render json: { errors: @user.errors.full_messages.to_sentence }, status: :bad_request @@ -42,6 +44,12 @@ def destroy private + def get_user_course_hash(user_ids) + ActsAsTenant.without_tenant do + CourseUser.includes(:course).where(user_id: user_ids).group_by(&:user_id) + end + end + def user_params params.require(:user).permit(:name, :role) end diff --git a/app/views/system/admin/instance/users/_user_list_data.json.jbuilder b/app/views/system/admin/instance/users/_user_list_data.json.jbuilder index 4de455110a8..ea14e504fc7 100644 --- a/app/views/system/admin/instance/users/_user_list_data.json.jbuilder +++ b/app/views/system/admin/instance/users/_user_list_data.json.jbuilder @@ -4,4 +4,7 @@ json.userId instance_user.user.id json.name instance_user.user.name json.email instance_user.user.email json.role instance_user.role -json.courses instance_user.user.courses.count +json.courses instance_user.user.courses.each do |course| + json.id course.id + json.title course.title +end diff --git a/app/views/system/admin/users/_user_list_data.json.jbuilder b/app/views/system/admin/users/_user_list_data.json.jbuilder index e3b03732577..20ce0f46fb6 100644 --- a/app/views/system/admin/users/_user_list_data.json.jbuilder +++ b/app/views/system/admin/users/_user_list_data.json.jbuilder @@ -3,8 +3,14 @@ json.id user.id json.name user.name json.email user.email + +courses_by_instance = course_users.group_by { |cu| cu.course.instance_id } json.instances @instances_preload_service.instances_for(user.id)&.each do |instance| json.name instance.name json.host instance.host + json.courses courses_by_instance.fetch(instance.id, []) do |course_user| + json.id course_user.course.id + json.title course_user.course.title + end end json.role user.role diff --git a/app/views/system/admin/users/index.json.jbuilder b/app/views/system/admin/users/index.json.jbuilder index 62441547402..d3a24f9629b 100644 --- a/app/views/system/admin/users/index.json.jbuilder +++ b/app/views/system/admin/users/index.json.jbuilder @@ -1,6 +1,6 @@ # frozen_string_literal: true json.users @users.each do |user| - json.partial! 'user_list_data', user: user + json.partial! 'user_list_data', user: user, course_users: @user_course_hash.fetch(user.id, []) end json.counts do diff --git a/client/app/bundles/system/admin/admin/components/buttons/UsersButtons.tsx b/client/app/bundles/system/admin/admin/components/buttons/UsersButtons.tsx index 8df9779c917..f1835561022 100644 --- a/client/app/bundles/system/admin/admin/components/buttons/UsersButtons.tsx +++ b/client/app/bundles/system/admin/admin/components/buttons/UsersButtons.tsx @@ -1,5 +1,5 @@ import { FC, memo, useState } from 'react'; -import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import { defineMessages } from 'react-intl'; import equal from 'fast-deep-equal'; import { UserMiniEntity } from 'types/users'; @@ -12,7 +12,7 @@ import useTranslation from 'lib/hooks/useTranslation'; import { deleteUser } from '../../operations'; -interface Props extends WrappedComponentProps { +interface Props { user: UserMiniEntity; } @@ -27,20 +27,20 @@ const translations = defineMessages({ }, deletionConfirmTitle: { id: 'system.admin.admin.UsersButton.deletionConfirmTitle', - defaultMessage: 'Deleting {role} {name} ({email})', + defaultMessage: 'Deleting {role} User {name} ({email})', }, deletionPromptContent: { id: 'system.admin.admin.UsersButton.deletionPromptContent', defaultMessage: - 'After deleting this user, all associated instance users in the following instances will be deleted.', + 'Deleting this user will PERMANENTLY delete associated data in the following {count, plural, one {course} other {courses}}:', }, - associatedInstances: { - id: 'system.admin.admin.UsersButton.associatedInstances', - defaultMessage: '{index}. {instanceName}', + associatedCourses: { + id: 'system.admin.admin.UsersButton.associatedCourses', + defaultMessage: '{courseName} ({instanceName})', }, deletionConfirm: { id: 'system.admin.admin.UsersButton.deletionConfirm', - defaultMessage: 'Are you sure?', + defaultMessage: 'Are you sure you wish to proceed?', }, deleteTooltip: { id: 'system.admin.admin.UsersButton.deleteTooltip', @@ -49,16 +49,23 @@ const translations = defineMessages({ }); const UserManagementButtons: FC = (props) => { - const { intl, user } = props; + const { user } = props; const dispatch = useAppDispatch(); const [isDeleting, setIsDeleting] = useState(false); const { t } = useTranslation(); + const userCoursesWithInstanceNames = user.instances.flatMap((instance) => + instance.courses.map((course) => ({ + ...course, + instanceName: instance.name, + })), + ); + const onDelete = (): Promise => { setIsDeleting(true); return dispatch(deleteUser(user.id)) .then(() => { - toast.success(intl.formatMessage(translations.deletionSuccess)); + toast.success(t(translations.deletionSuccess)); }) .catch((error) => { setIsDeleting(false); @@ -66,7 +73,7 @@ const UserManagementButtons: FC = (props) => { ? error.response.data.errors : ''; toast.error( - intl.formatMessage(translations.deletionFailure, { + t(translations.deletionFailure, { error: errorMessage, }), ); @@ -88,17 +95,25 @@ const UserManagementButtons: FC = (props) => { })} tooltip={t(translations.deleteTooltip)} > - {user.instances.length > 1 && ( + {userCoursesWithInstanceNames.length > 0 && ( <> - {t(translations.deletionPromptContent)} - {user.instances.map((instance, index) => ( - - {t(translations.associatedInstances, { - index: index + 1, - instanceName: instance.name, - })} - - ))} + + {t(translations.deletionPromptContent, { + count: userCoursesWithInstanceNames.length, + })} + +
    + {userCoursesWithInstanceNames.map((course) => ( + +
  1. + {t(translations.associatedCourses, { + instanceName: course.instanceName, + courseName: course.title, + })} +
  2. +
    + ))} +
)} {t(translations.deletionConfirm)} @@ -107,9 +122,6 @@ const UserManagementButtons: FC = (props) => { ); }; -export default memo( - injectIntl(UserManagementButtons), - (prevProps, nextProps) => { - return equal(prevProps.user, nextProps.user); - }, -); +export default memo(UserManagementButtons, (prevProps, nextProps) => { + return equal(prevProps.user, nextProps.user); +}); diff --git a/client/app/bundles/system/admin/admin/components/tables/UsersTable.tsx b/client/app/bundles/system/admin/admin/components/tables/UsersTable.tsx index 960c42488ad..f01116aa78c 100644 --- a/client/app/bundles/system/admin/admin/components/tables/UsersTable.tsx +++ b/client/app/bundles/system/admin/admin/components/tables/UsersTable.tsx @@ -1,5 +1,5 @@ import { FC, ReactElement, useState } from 'react'; -import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import { defineMessages } from 'react-intl'; import { CircularProgress, MenuItem, @@ -25,11 +25,12 @@ import { import rebuildObjectFromRow from 'lib/helpers/mui-datatables-helpers'; import { useAppDispatch } from 'lib/hooks/store'; import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; import tableTranslations from 'lib/translations/table'; import { indexUsers, updateUser } from '../../operations'; -interface Props extends WrappedComponentProps { +interface Props { users: UserMiniEntity[]; userCounts: AdminStats; filter: { active: boolean; role: string }; @@ -62,13 +63,18 @@ const translations = defineMessages({ id: 'system.admin.users.UsersTable.fetchFilteredUsersFailure', defaultMessage: 'Failed to fetch users.', }, + userInstanceEntry: { + id: 'system.admin.users.UsersTable.instanceEntry', + defaultMessage: + '{instanceName}{courseCount, plural, =0 {} one { (1 course)} other { ({courseCount} courses)}}', + }, }); const UsersTable: FC = (props) => { - const { title, renderRowActionComponent, intl, filter, users, userCounts } = - props; + const { title, renderRowActionComponent, filter, users, userCounts } = props; const [isLoading, setIsLoading] = useState(false); const dispatch = useAppDispatch(); + const { t } = useTranslation(); const [tableState, setTableState] = useState({ count: userCounts.usersCount, @@ -88,14 +94,14 @@ const UsersTable: FC = (props) => { return dispatch(updateUser(user.id, newUser)) .then(() => { toast.success( - intl.formatMessage(translations.renameSuccess, { + t(translations.renameSuccess, { oldName: user.name, newName, }), ); }) .catch((error) => { - toast.error(intl.formatMessage(translations.updateNameFailure)); + toast.error(t(translations.updateNameFailure)); throw error; }); }; @@ -117,14 +123,14 @@ const UsersTable: FC = (props) => { .then(() => { updateValue(newRole); toast.success( - intl.formatMessage(translations.changeRoleSuccess, { + t(translations.changeRoleSuccess, { name: user.name, role: USER_ROLES[newRole], }), ); }) .catch(() => { - toast.error(intl.formatMessage(translations.updateRoleFailure)); + toast.error(t(translations.updateRoleFailure)); }); }; @@ -142,9 +148,7 @@ const UsersTable: FC = (props) => { active: filter.active, }), ) - .catch(() => - toast.error(intl.formatMessage(translations.fetchFilteredUsersFailure)), - ) + .catch(() => toast.error(t(translations.fetchFilteredUsersFailure))) .finally(() => { setIsLoading(false); }); @@ -165,9 +169,7 @@ const UsersTable: FC = (props) => { search: searchText ? searchText.trim() : searchText, }), ) - .catch(() => - toast.error(intl.formatMessage(translations.fetchFilteredUsersFailure)), - ) + .catch(() => toast.error(t(translations.fetchFilteredUsersFailure))) .finally(() => { setIsLoading(false); }); @@ -196,7 +198,7 @@ const UsersTable: FC = (props) => { rowsPerPage: DEFAULT_TABLE_ROWS_PER_PAGE, rowsPerPageOptions: [DEFAULT_TABLE_ROWS_PER_PAGE], search: true, - searchPlaceholder: intl.formatMessage(translations.searchText), + searchPlaceholder: t(translations.searchText), selectableRows: 'none', serverSide: true, setTableProps: (): Record => { @@ -224,7 +226,7 @@ const UsersTable: FC = (props) => { }, { name: 'name', - label: intl.formatMessage(tableTranslations.name), + label: t(tableTranslations.name), options: { alignCenter: false, sort: false, @@ -247,7 +249,7 @@ const UsersTable: FC = (props) => { }, { name: 'email', - label: intl.formatMessage(tableTranslations.email), + label: t(tableTranslations.email), options: { alignCenter: false, sort: false, @@ -267,7 +269,7 @@ const UsersTable: FC = (props) => { }, { name: 'instances', - label: intl.formatMessage(tableTranslations.instances), + label: t(tableTranslations.instances), options: { alignCenter: false, sort: false, @@ -281,7 +283,10 @@ const UsersTable: FC = (props) => { href={`//${instance.host}/admin/users`} underline="hover" > - {instance.name} + {t(translations.userInstanceEntry, { + instanceName: instance.name, + courseCount: instance.courses.length, + })} ))} @@ -292,7 +297,7 @@ const UsersTable: FC = (props) => { }, { name: 'role', - label: intl.formatMessage(tableTranslations.role), + label: t(tableTranslations.role), options: { alignCenter: false, sort: false, @@ -326,7 +331,7 @@ const UsersTable: FC = (props) => { }, { name: 'actions', - label: intl.formatMessage(tableTranslations.actions), + label: t(tableTranslations.actions), options: { empty: true, sort: false, @@ -359,4 +364,4 @@ const UsersTable: FC = (props) => { ); }; -export default injectIntl(UsersTable); +export default UsersTable; diff --git a/client/app/bundles/system/admin/instance/instance/InstanceAdminNavigator.tsx b/client/app/bundles/system/admin/instance/instance/InstanceAdminNavigator.tsx index a0bd49897fe..bdacdaeab63 100644 --- a/client/app/bundles/system/admin/instance/instance/InstanceAdminNavigator.tsx +++ b/client/app/bundles/system/admin/instance/instance/InstanceAdminNavigator.tsx @@ -114,7 +114,7 @@ const InstanceAdminNavigator = (): JSX.Element => { const handle: DataHandle = () => ({ getData: async (): Promise => { const data = await fetchInstance(); - return `${data.name} Admin Panel`; + return `${data.name} Instance Admin Panel`; }, }); diff --git a/client/app/bundles/system/admin/instance/instance/components/buttons/UsersButtons.tsx b/client/app/bundles/system/admin/instance/instance/components/buttons/UsersButtons.tsx index d12289dc1fa..e88288344ee 100644 --- a/client/app/bundles/system/admin/instance/instance/components/buttons/UsersButtons.tsx +++ b/client/app/bundles/system/admin/instance/instance/components/buttons/UsersButtons.tsx @@ -1,48 +1,60 @@ import { FC, memo, useState } from 'react'; -import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import { defineMessages } from 'react-intl'; import equal from 'fast-deep-equal'; import { InstanceUserMiniEntity } from 'types/system/instance/users'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; +import { PromptText } from 'lib/components/core/dialogs/Prompt'; import { USER_ROLES } from 'lib/constants/sharedConstants'; import { useAppDispatch } from 'lib/hooks/store'; import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; import { deleteUser } from '../../operations'; -interface Props extends WrappedComponentProps { +interface Props { user: InstanceUserMiniEntity; } const translations = defineMessages({ deletionSuccess: { id: 'system.admin.instance.instance.UsersButton.deletionSuccess', - defaultMessage: 'User was deleted.', + defaultMessage: 'User was removed from this instance.', }, deletionFailure: { id: 'system.admin.instance.instance.UsersButton.deletionFailure', - defaultMessage: 'Failed to delete user - {error}', + defaultMessage: 'Failed to remove user - {error}', + }, + deletionConfirmTitle: { + id: 'system.admin.instance.instance.UsersButton.deletionConfirmTitle', + defaultMessage: 'Removing {role} User {name} ({email})', + }, + deletionPromptContent: { + id: 'system.admin.instance.instance.UsersButton.deletionPromptContent', + defaultMessage: + 'Removing this user may cause errors in the following {count, plural, one {course} other {courses}}:', }, deletionConfirm: { id: 'system.admin.instance.instance.UsersButton.deletionConfirm', - defaultMessage: 'Are you sure you wish to delete {role} {name} ({email})?', + defaultMessage: 'Are you sure you wish to proceed?', }, deleteTooltip: { id: 'system.admin.instance.instance.UsersButton.deleteTooltip', - defaultMessage: 'Delete User', + defaultMessage: 'Remove User', }, }); const UserManagementButtons: FC = (props) => { - const { intl, user } = props; + const { user } = props; const dispatch = useAppDispatch(); + const { t } = useTranslation(); const [isDeleting, setIsDeleting] = useState(false); const onDelete = (): Promise => { setIsDeleting(true); return dispatch(deleteUser(user.id)) .then(() => { - toast.success(intl.formatMessage(translations.deletionSuccess)); + toast.success(t(translations.deletionSuccess)); }) .catch((error) => { setIsDeleting(false); @@ -50,7 +62,7 @@ const UserManagementButtons: FC = (props) => { ? error.response.data.errors : ''; toast.error( - intl.formatMessage(translations.deletionFailure, { + t(translations.deletionFailure, { error: errorMessage, }), ); @@ -62,23 +74,38 @@ const UserManagementButtons: FC = (props) => {
+ tooltip={t(translations.deleteTooltip)} + > + {user.courses.length > 0 && ( + <> + + {t(translations.deletionPromptContent, { + count: user.courses.length, + })} + +
    + {user.courses.map((course) => ( + +
  1. {course.title}
  2. +
    + ))} +
+ + )} + {t(translations.deletionConfirm)} +
); }; -export default memo( - injectIntl(UserManagementButtons), - (prevProps, nextProps) => { - return equal(prevProps.user, nextProps.user); - }, -); +export default memo(UserManagementButtons, (prevProps, nextProps) => { + return equal(prevProps.user, nextProps.user); +}); diff --git a/client/app/bundles/system/admin/instance/instance/components/tables/UsersTable.tsx b/client/app/bundles/system/admin/instance/instance/components/tables/UsersTable.tsx index 888a77a17e0..4ec5806f04e 100644 --- a/client/app/bundles/system/admin/instance/instance/components/tables/UsersTable.tsx +++ b/client/app/bundles/system/admin/instance/instance/components/tables/UsersTable.tsx @@ -1,5 +1,5 @@ import { FC, ReactElement, useState } from 'react'; -import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import { defineMessages } from 'react-intl'; import { CircularProgress, MenuItem, @@ -28,11 +28,12 @@ import { import rebuildObjectFromRow from 'lib/helpers/mui-datatables-helpers'; import { useAppDispatch } from 'lib/hooks/store'; import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; import tableTranslations from 'lib/translations/table'; import { indexUsers, updateUser } from '../../operations'; -interface Props extends WrappedComponentProps { +interface Props { users: InstanceUserMiniEntity[]; userCounts: InstanceAdminStats; title: string; @@ -68,10 +69,10 @@ const translations = defineMessages({ }); const UsersTable: FC = (props) => { - const { title, renderRowActionComponent, intl, users, userCounts, filter } = - props; + const { title, renderRowActionComponent, users, userCounts, filter } = props; const [isLoading, setIsLoading] = useState(false); const dispatch = useAppDispatch(); + const { t } = useTranslation(); const [tableState, setTableState] = useState({ count: userCounts.usersCount, @@ -96,14 +97,14 @@ const UsersTable: FC = (props) => { .then(() => { updateValue(newRole); toast.success( - intl.formatMessage(translations.changeRoleSuccess, { + t(translations.changeRoleSuccess, { name: user.name, role: INSTANCE_USER_ROLES[newRole], }), ); }) .catch(() => { - toast.error(intl.formatMessage(translations.updateRoleFailure)); + toast.error(t(translations.updateRoleFailure)); }); }; @@ -121,9 +122,7 @@ const UsersTable: FC = (props) => { active: filter.active, }), ) - .catch(() => - toast.error(intl.formatMessage(translations.fetchFilteredUsersFailure)), - ) + .catch(() => toast.error(t(translations.fetchFilteredUsersFailure))) .finally(() => { setIsLoading(false); }); @@ -144,9 +143,7 @@ const UsersTable: FC = (props) => { search: searchText ? searchText.trim() : searchText, }), ) - .catch(() => - toast.error(intl.formatMessage(translations.fetchFilteredUsersFailure)), - ) + .catch(() => toast.error(t(translations.fetchFilteredUsersFailure))) .finally(() => { setIsLoading(false); }); @@ -175,7 +172,7 @@ const UsersTable: FC = (props) => { rowsPerPage: DEFAULT_TABLE_ROWS_PER_PAGE, rowsPerPageOptions: [DEFAULT_TABLE_ROWS_PER_PAGE], search: true, - searchPlaceholder: intl.formatMessage(translations.searchText), + searchPlaceholder: t(translations.searchText), selectableRows: 'none', serverSide: true, setTableProps: (): Record => { @@ -212,7 +209,7 @@ const UsersTable: FC = (props) => { }, { name: 'name', - label: intl.formatMessage(tableTranslations.name), + label: t(tableTranslations.name), options: { alignCenter: false, sort: false, @@ -220,7 +217,7 @@ const UsersTable: FC = (props) => { }, { name: 'email', - label: intl.formatMessage(tableTranslations.email), + label: t(tableTranslations.email), options: { alignCenter: false, sort: false, @@ -240,7 +237,7 @@ const UsersTable: FC = (props) => { }, { name: 'courses', - label: intl.formatMessage(tableTranslations.relatedCourses), + label: t(tableTranslations.relatedCourses), options: { alignCenter: false, sort: false, @@ -254,7 +251,7 @@ const UsersTable: FC = (props) => { to={`/users/${user.userId}`} underline="hover" > - {user.courses} + {user.courses.length} ); }, @@ -262,7 +259,7 @@ const UsersTable: FC = (props) => { }, { name: 'role', - label: intl.formatMessage(tableTranslations.role), + label: t(tableTranslations.role), options: { alignCenter: false, sort: false, @@ -296,7 +293,7 @@ const UsersTable: FC = (props) => { }, { name: 'actions', - label: intl.formatMessage(tableTranslations.actions), + label: t(tableTranslations.actions), options: { empty: true, sort: false, @@ -329,4 +326,4 @@ const UsersTable: FC = (props) => { ); }; -export default injectIntl(UsersTable); +export default UsersTable; diff --git a/client/app/types/system/instance/users.ts b/client/app/types/system/instance/users.ts index c77fd384115..b2225ab265e 100644 --- a/client/app/types/system/instance/users.ts +++ b/client/app/types/system/instance/users.ts @@ -8,7 +8,10 @@ export interface InstanceUserListData { name: string; email: string; role: InstanceUserRoles; - courses: number; + courses: { + id: number; + title: string; + }[]; } export interface InstanceUserBasicListData { @@ -36,7 +39,10 @@ export interface InstanceUserBasicPhotoMiniEntity export interface InstanceUserMiniEntity extends InstanceUserBasicMiniEntity { email: string; role: InstanceUserRoles; - courses: number; + courses: { + id: number; + title: string; + }[]; } export interface InstanceAdminStats { diff --git a/client/app/types/users.ts b/client/app/types/users.ts index a8490056475..f741a737467 100644 --- a/client/app/types/users.ts +++ b/client/app/types/users.ts @@ -14,6 +14,10 @@ export interface UserListData { instances: { name: string; host: string; + courses: { + id: number; + title: string; + }[]; }[]; role: UserRoles; } @@ -29,6 +33,10 @@ export interface UserMiniEntity extends UserBasicMiniEntity { instances: { name: string; host: string; + courses: { + id: number; + title: string; + }[]; }[]; role: UserRoles; } diff --git a/client/locales/en.json b/client/locales/en.json index a65526fb832..e5da567ec87 100644 --- a/client/locales/en.json +++ b/client/locales/en.json @@ -7535,6 +7535,12 @@ "system.admin.admin.UsersButton.deletionConfirm": { "defaultMessage": "Are you sure you wish to delete {role} {name} ({email})?" }, + "system.admin.admin.UsersButton.deletionConfirmTitle": { + "defaultMessage": "Deleting {role} User {name} ({email})" + }, + "system.admin.admin.UsersButton.deletionPromptContent": { + "defaultMessage": "Deleting this user will PERMANENTLY delete associated data in the following {count, plural, one {course} other {courses}}:" + }, "system.admin.admin.UsersButton.deletionFailure": { "defaultMessage": "Failed to delete user - {error}" }, @@ -7920,16 +7926,22 @@ "defaultMessage": "pending" }, "system.admin.instance.instance.UsersButton.deleteTooltip": { - "defaultMessage": "Delete User" + "defaultMessage": "Remove User" }, "system.admin.instance.instance.UsersButton.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete {role} {name} ({email})?" + "defaultMessage": "Are you sure you wish to proceed?" + }, + "system.admin.instance.instance.UsersButton.deletionConfirmTitle": { + "defaultMessage": "Removing {role} User {name} ({email})" + }, + "system.admin.instance.instance.UsersButton.deletionPromptContent": { + "defaultMessage": "Removing this user may cause errors in the following {count, plural, one {course} other {courses}}:" }, "system.admin.instance.instance.UsersButton.deletionFailure": { - "defaultMessage": "Failed to delete user - {error}" + "defaultMessage": "Failed to remove user - {error}" }, "system.admin.instance.instance.UsersButton.deletionSuccess": { - "defaultMessage": "User was deleted." + "defaultMessage": "User was removed from this instance." }, "system.admin.instance.instance.UsersTable.changeRoleSuccess": { "defaultMessage": "Successfully changed {name}'s role to {role}." @@ -7952,6 +7964,9 @@ "system.admin.users.UsersTable.fetchFilteredUsersFailure": { "defaultMessage": "Failed to fetch users." }, + "system.admin.users.UsersTable.instanceEntry": { + "defaultMessage": "{instanceName}{courseCount, plural, =0 {} one { (1 course)} other { ({courseCount} courses)}}" + }, "user.accountSettings": { "defaultMessage": "Account Settings" }, diff --git a/client/locales/ko.json b/client/locales/ko.json index 9c29fde2a3e..fba49cbe4f5 100644 --- a/client/locales/ko.json +++ b/client/locales/ko.json @@ -7530,14 +7530,20 @@ "defaultMessage": "사용자 삭제" }, "system.admin.admin.UsersButton.deletionConfirm": { - "defaultMessage": "{role} {name} ({email})을(를) 삭제하시겠습니까?" + "defaultMessage": "정말로 계속하시겠습니까?" }, "system.admin.admin.UsersButton.deletionFailure": { - "defaultMessage": "사용자를 삭제하지 못했습니다 - {error}" + "defaultMessage": "사용자 삭제에 실패했습니다 - {error}" }, "system.admin.admin.UsersButton.deletionSuccess": { "defaultMessage": "사용자가 삭제되었습니다." }, + "system.admin.admin.UsersButton.deletionConfirmTitle": { + "defaultMessage": "{role} 사용자 {name} ({email}) 삭제 중" + }, + "system.admin.admin.UsersButton.deletionPromptContent": { + "defaultMessage": "이 사용자를 삭제하면 다음 {count, plural, one {과목} other {과목들}}에 연결된 데이터가 영구적으로 삭제됩니다:" + }, "system.admin.admin.UsersIndex.activeUsers": { "defaultMessage": "활성 사용자: {allCount} ({adminCount} 관리자, {normalCount} 일반){br}(지난 7일 동안 활성)" }, @@ -7553,6 +7559,9 @@ "system.admin.admin.UsersTable.changeRoleSuccess": { "defaultMessage": "{name}의 역할이 {role}(으)로 변경되었습니다." }, + "system.admin.users.UsersTable.instanceEntry": { + "defaultMessage": "{instanceName}{courseCount, plural, =0 {} one { (1개 과목)} other { ({courseCount}개 과목)}}" + }, "system.admin.admin.UsersTable.renameSuccess": { "defaultMessage": "{oldName}이(가) {newName}(으)로 이름이 변경되었습니다." }, @@ -7917,16 +7926,22 @@ "defaultMessage": "대기 중" }, "system.admin.instance.instance.UsersButton.deleteTooltip": { - "defaultMessage": "사용자 삭제" + "defaultMessage": "사용자 제거" }, "system.admin.instance.instance.UsersButton.deletionConfirm": { - "defaultMessage": "{role} {name} ({email})을(를) 삭제하시겠습니까?" + "defaultMessage": "정말로 계속하시겠습니까?" }, "system.admin.instance.instance.UsersButton.deletionFailure": { - "defaultMessage": "사용자를 삭제하지 못했습니다 - {error}" + "defaultMessage": "사용자 제거에 실패했습니다 - {error}" }, "system.admin.instance.instance.UsersButton.deletionSuccess": { - "defaultMessage": "사용자가 삭제되었습니다." + "defaultMessage": "사용자가 이 인스턴스에서 제거되었습니다." + }, + "system.admin.instance.instance.UsersButton.deletionConfirmTitle": { + "defaultMessage": "{role} 사용자 {name} ({email}) 제거 중" + }, + "system.admin.instance.instance.UsersButton.deletionPromptContent": { + "defaultMessage": "이 사용자를 제거하면 다음 {count, plural, one {과목} other {과목들}}에서 오류가 발생할 수 있습니다:" }, "system.admin.instance.instance.UsersTable.changeRoleSuccess": { "defaultMessage": "{name}의 역할이 {role}(으)로 성공적으로 변경되었습니다." diff --git a/client/locales/zh.json b/client/locales/zh.json index 8e32e0ff416..7aa83c1c101 100644 --- a/client/locales/zh.json +++ b/client/locales/zh.json @@ -7491,14 +7491,20 @@ "defaultMessage": "删除用户" }, "system.admin.admin.UsersButton.deletionConfirm": { - "defaultMessage": "你确定要删除{role} {name} ({email}) 吗?" + "defaultMessage": "您确定要继续此操作吗?" }, "system.admin.admin.UsersButton.deletionFailure": { - "defaultMessage": "无法删除用户 - {error}" + "defaultMessage": "删除用户失败 - {error}" }, "system.admin.admin.UsersButton.deletionSuccess": { "defaultMessage": "用户已被删除。" }, + "system.admin.admin.UsersButton.deletionConfirmTitle": { + "defaultMessage": "正在删除{role}用户 {name}({email})" + }, + "system.admin.admin.UsersButton.deletionPromptContent": { + "defaultMessage": "删除该用户将永久删除以下{count, plural, one {课程} other {课程}}中的相关数据:" + }, "system.admin.admin.UsersIndex.activeUsers": { "defaultMessage": "活跃用户:{allCount}({adminCount} 管理员,{normalCount} 普通用户){br}(过去7天内活跃)" }, @@ -7514,6 +7520,9 @@ "system.admin.admin.UsersTable.changeRoleSuccess": { "defaultMessage": "已成功将 {name} 的角色更改为 {role}。" }, + "system.admin.users.UsersTable.instanceEntry": { + "defaultMessage": "{instanceName}{courseCount, plural, =0 {} one {(1 门课程)} other {({courseCount} 门课程)}}" + }, "system.admin.admin.UsersTable.renameSuccess": { "defaultMessage": "{oldName} 已重命名为 {newName}。" }, @@ -7878,16 +7887,22 @@ "defaultMessage": "待处理" }, "system.admin.instance.instance.UsersButton.deleteTooltip": { - "defaultMessage": "删除用户" + "defaultMessage": "移除用户" }, "system.admin.instance.instance.UsersButton.deletionConfirm": { - "defaultMessage": "你确定要删除{role} {name} ({email}) 吗?" + "defaultMessage": "您确定要继续此操作吗?" }, "system.admin.instance.instance.UsersButton.deletionFailure": { "defaultMessage": "无法删除用户 - {error}" }, "system.admin.instance.instance.UsersButton.deletionSuccess": { - "defaultMessage": "用户已被删除。" + "defaultMessage": "用户已从该实例中移除。" + }, + "system.admin.instance.instance.UsersButton.deletionConfirmTitle": { + "defaultMessage": "正在移除{role}用户 {name}({email})" + }, + "system.admin.instance.instance.UsersButton.deletionPromptContent": { + "defaultMessage": "移除该用户可能会导致以下{count, plural, one {课程} other {课程}}出现错误:" }, "system.admin.instance.instance.UsersTable.changeRoleSuccess": { "defaultMessage": "已成功将 {name} 的角色更改为 {role}。" diff --git a/spec/features/system/admin/instance/user_management_spec.rb b/spec/features/system/admin/instance/user_management_spec.rb index 08b4c1ab266..e1878d97302 100644 --- a/spec/features/system/admin/instance/user_management_spec.rb +++ b/spec/features/system/admin/instance/user_management_spec.rb @@ -67,7 +67,7 @@ def search_for_users(query, click: true) user_to_delete = instance_users.sample find("button.user-delete-#{user_to_delete.id}").click accept_prompt - expect_toastify('User was deleted.') + expect_toastify('User was removed from this instance.') end # Generate new users to search so it doesn't conflict with above scenarios