Skip to content

Commit 5f59ddb

Browse files
adi-herwana-nuscysjonathan
authored andcommitted
fix(admin): clarified user/instance_user deletion tooltips
1 parent 4e4b5ba commit 5f59ddb

File tree

15 files changed

+192
-81
lines changed

15 files changed

+192
-81
lines changed

app/controllers/system/admin/users_controller.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ def index
77
format.json do
88
load_users
99
load_counts
10-
@instances_preload_service = User::InstancePreloadService.new(@users.map(&:id))
10+
user_ids = @users.map(&:id)
11+
@instances_preload_service = User::InstancePreloadService.new(user_ids)
12+
@user_course_hash = get_user_course_hash(user_ids)
1113
end
1214
end
1315
end
@@ -16,7 +18,7 @@ def update
1618
@instances_preload_service = User::InstancePreloadService.new(@user.id)
1719
if @user.update(user_params)
1820
render 'system/admin/users/_user_list_data',
19-
locals: { user: @user },
21+
locals: { user: @user, course_users: get_user_course_hash([@user.id]).fetch(@user.id, []) },
2022
status: :ok
2123
else
2224
render json: { errors: @user.errors.full_messages.to_sentence }, status: :bad_request
@@ -42,6 +44,12 @@ def destroy
4244

4345
private
4446

47+
def get_user_course_hash(user_ids)
48+
ActsAsTenant.without_tenant do
49+
CourseUser.includes(:course).where(user_id: user_ids).group_by(&:user_id)
50+
end
51+
end
52+
4553
def user_params
4654
params.require(:user).permit(:name, :role)
4755
end

app/views/system/admin/instance/users/_user_list_data.json.jbuilder

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ json.userId instance_user.user.id
44
json.name instance_user.user.name
55
json.email instance_user.user.email
66
json.role instance_user.role
7-
json.courses instance_user.user.courses.count
7+
json.courses instance_user.user.courses.each do |course|
8+
json.id course.id
9+
json.title course.title
10+
end

app/views/system/admin/users/_user_list_data.json.jbuilder

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
json.id user.id
44
json.name user.name
55
json.email user.email
6+
7+
courses_by_instance = course_users.group_by { |cu| cu.course.instance_id }
68
json.instances @instances_preload_service.instances_for(user.id)&.each do |instance|
79
json.name instance.name
810
json.host instance.host
11+
json.courses courses_by_instance.fetch(instance.id, []) do |course_user|
12+
json.id course_user.course.id
13+
json.title course_user.course.title
14+
end
915
end
1016
json.role user.role

app/views/system/admin/users/index.json.jbuilder

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22
json.users @users.each do |user|
3-
json.partial! 'user_list_data', user: user
3+
json.partial! 'user_list_data', user: user, course_users: @user_course_hash.fetch(user.id, [])
44
end
55

66
json.counts do

client/app/bundles/system/admin/admin/components/buttons/UsersButtons.tsx

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,20 @@ const translations = defineMessages({
2727
},
2828
deletionConfirmTitle: {
2929
id: 'system.admin.admin.UsersButton.deletionConfirmTitle',
30-
defaultMessage: 'Deleting {role} {name} ({email})',
30+
defaultMessage: 'Deleting {role} User {name} ({email})',
3131
},
3232
deletionPromptContent: {
3333
id: 'system.admin.admin.UsersButton.deletionPromptContent',
3434
defaultMessage:
35-
'After deleting this user, all associated instance users in the following instances will be deleted.',
35+
'Deleting this user will PERMANENTLY delete associated data in the following {count, plural, one {course} other {courses}}:',
3636
},
37-
associatedInstances: {
38-
id: 'system.admin.admin.UsersButton.associatedInstances',
39-
defaultMessage: '{index}. {instanceName}',
37+
associatedCourses: {
38+
id: 'system.admin.admin.UsersButton.associatedCourses',
39+
defaultMessage: '{courseName} ({instanceName})',
4040
},
4141
deletionConfirm: {
4242
id: 'system.admin.admin.UsersButton.deletionConfirm',
43-
defaultMessage: 'Are you sure?',
43+
defaultMessage: 'Are you sure you wish to proceed?',
4444
},
4545
deleteTooltip: {
4646
id: 'system.admin.admin.UsersButton.deleteTooltip',
@@ -54,6 +54,13 @@ const UserManagementButtons: FC<Props> = (props) => {
5454
const [isDeleting, setIsDeleting] = useState(false);
5555
const { t } = useTranslation();
5656

57+
const userCoursesWithInstanceNames = user.instances.flatMap((instance) =>
58+
instance.courses.map((course) => ({
59+
...course,
60+
instanceName: instance.name,
61+
})),
62+
);
63+
5764
const onDelete = (): Promise<void> => {
5865
setIsDeleting(true);
5966
return dispatch(deleteUser(user.id))
@@ -88,17 +95,25 @@ const UserManagementButtons: FC<Props> = (props) => {
8895
})}
8996
tooltip={t(translations.deleteTooltip)}
9097
>
91-
{user.instances.length > 1 && (
98+
{userCoursesWithInstanceNames.length > 0 && (
9299
<>
93-
<PromptText>{t(translations.deletionPromptContent)}</PromptText>
94-
{user.instances.map((instance, index) => (
95-
<PromptText key={`instance-${instance.host}`}>
96-
{t(translations.associatedInstances, {
97-
index: index + 1,
98-
instanceName: instance.name,
99-
})}
100-
</PromptText>
101-
))}
100+
<PromptText>
101+
{t(translations.deletionPromptContent, {
102+
count: userCoursesWithInstanceNames.length,
103+
})}
104+
</PromptText>
105+
<ol>
106+
{userCoursesWithInstanceNames.map((course) => (
107+
<PromptText key={`course-${course.id}`}>
108+
<li>
109+
{t(translations.associatedCourses, {
110+
instanceName: course.instanceName,
111+
courseName: course.title,
112+
})}
113+
</li>
114+
</PromptText>
115+
))}
116+
</ol>
102117
</>
103118
)}
104119
<PromptText>{t(translations.deletionConfirm)}</PromptText>
@@ -107,9 +122,6 @@ const UserManagementButtons: FC<Props> = (props) => {
107122
);
108123
};
109124

110-
export default memo(
111-
UserManagementButtons,
112-
(prevProps, nextProps) => {
113-
return equal(prevProps.user, nextProps.user);
114-
},
115-
);
125+
export default memo(UserManagementButtons, (prevProps, nextProps) => {
126+
return equal(prevProps.user, nextProps.user);
127+
});

client/app/bundles/system/admin/admin/components/tables/UsersTable.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import {
2525
import rebuildObjectFromRow from 'lib/helpers/mui-datatables-helpers';
2626
import { useAppDispatch } from 'lib/hooks/store';
2727
import toast from 'lib/hooks/toast';
28+
import useTranslation from 'lib/hooks/useTranslation';
2829
import tableTranslations from 'lib/translations/table';
2930

3031
import { indexUsers, updateUser } from '../../operations';
31-
import useTranslation from 'lib/hooks/useTranslation';
3232

3333
interface Props {
3434
users: UserMiniEntity[];
@@ -63,11 +63,15 @@ const translations = defineMessages({
6363
id: 'system.admin.users.UsersTable.fetchFilteredUsersFailure',
6464
defaultMessage: 'Failed to fetch users.',
6565
},
66+
userInstanceEntry: {
67+
id: 'system.admin.users.UsersTable.instanceEntry',
68+
defaultMessage:
69+
'{instanceName}{courseCount, plural, =0 {} one { (1 course)} other { ({courseCount} courses)}}',
70+
},
6671
});
6772

6873
const UsersTable: FC<Props> = (props) => {
69-
const { title, renderRowActionComponent, filter, users, userCounts } =
70-
props;
74+
const { title, renderRowActionComponent, filter, users, userCounts } = props;
7175
const [isLoading, setIsLoading] = useState(false);
7276
const dispatch = useAppDispatch();
7377
const { t } = useTranslation();
@@ -144,9 +148,7 @@ const UsersTable: FC<Props> = (props) => {
144148
active: filter.active,
145149
}),
146150
)
147-
.catch(() =>
148-
toast.error(t(translations.fetchFilteredUsersFailure)),
149-
)
151+
.catch(() => toast.error(t(translations.fetchFilteredUsersFailure)))
150152
.finally(() => {
151153
setIsLoading(false);
152154
});
@@ -167,9 +169,7 @@ const UsersTable: FC<Props> = (props) => {
167169
search: searchText ? searchText.trim() : searchText,
168170
}),
169171
)
170-
.catch(() =>
171-
toast.error(t(translations.fetchFilteredUsersFailure)),
172-
)
172+
.catch(() => toast.error(t(translations.fetchFilteredUsersFailure)))
173173
.finally(() => {
174174
setIsLoading(false);
175175
});
@@ -283,7 +283,10 @@ const UsersTable: FC<Props> = (props) => {
283283
href={`//${instance.host}/admin/users`}
284284
underline="hover"
285285
>
286-
{instance.name}
286+
{t(translations.userInstanceEntry, {
287+
instanceName: instance.name,
288+
courseCount: instance.courses.length,
289+
})}
287290
</Link>
288291
</li>
289292
))}

client/app/bundles/system/admin/instance/instance/InstanceAdminNavigator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ const InstanceAdminNavigator = (): JSX.Element => {
114114
const handle: DataHandle = () => ({
115115
getData: async (): Promise<string> => {
116116
const data = await fetchInstance();
117-
return `${data.name} Admin Panel`;
117+
return `${data.name} Instance Admin Panel`;
118118
},
119119
});
120120

client/app/bundles/system/admin/instance/instance/components/buttons/UsersButtons.tsx

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import equal from 'fast-deep-equal';
44
import { InstanceUserMiniEntity } from 'types/system/instance/users';
55

66
import DeleteButton from 'lib/components/core/buttons/DeleteButton';
7+
import { PromptText } from 'lib/components/core/dialogs/Prompt';
78
import { USER_ROLES } from 'lib/constants/sharedConstants';
89
import { useAppDispatch } from 'lib/hooks/store';
910
import toast from 'lib/hooks/toast';
11+
import useTranslation from 'lib/hooks/useTranslation';
1012

1113
import { deleteUser } from '../../operations';
12-
import useTranslation from 'lib/hooks/useTranslation';
1314

1415
interface Props {
1516
user: InstanceUserMiniEntity;
@@ -18,19 +19,28 @@ interface Props {
1819
const translations = defineMessages({
1920
deletionSuccess: {
2021
id: 'system.admin.instance.instance.UsersButton.deletionSuccess',
21-
defaultMessage: 'User was deleted.',
22+
defaultMessage: 'User was removed from this instance.',
2223
},
2324
deletionFailure: {
2425
id: 'system.admin.instance.instance.UsersButton.deletionFailure',
25-
defaultMessage: 'Failed to delete user - {error}',
26+
defaultMessage: 'Failed to remove user - {error}',
27+
},
28+
deletionConfirmTitle: {
29+
id: 'system.admin.instance.instance.UsersButton.deletionConfirmTitle',
30+
defaultMessage: 'Removing {role} User {name} ({email})',
31+
},
32+
deletionPromptContent: {
33+
id: 'system.admin.instance.instance.UsersButton.deletionPromptContent',
34+
defaultMessage:
35+
'Removing this user may cause errors in the following {count, plural, one {course} other {courses}}:',
2636
},
2737
deletionConfirm: {
2838
id: 'system.admin.instance.instance.UsersButton.deletionConfirm',
29-
defaultMessage: 'Are you sure you wish to delete {role} {name} ({email})?',
39+
defaultMessage: 'Are you sure you wish to proceed?',
3040
},
3141
deleteTooltip: {
3242
id: 'system.admin.instance.instance.UsersButton.deleteTooltip',
33-
defaultMessage: 'Delete User',
43+
defaultMessage: 'Remove User',
3444
},
3545
});
3646

@@ -64,23 +74,38 @@ const UserManagementButtons: FC<Props> = (props) => {
6474
<div key={`buttons-${user.id}`}>
6575
<DeleteButton
6676
className={`user-delete-${user.id} p-0`}
67-
confirmMessage={t(translations.deletionConfirm, {
77+
disabled={isDeleting}
78+
loading={isDeleting}
79+
onClick={onDelete}
80+
title={t(translations.deletionConfirmTitle, {
6881
role: USER_ROLES[user.role],
6982
name: user.name,
7083
email: user.email,
7184
})}
72-
disabled={isDeleting}
73-
loading={isDeleting}
74-
onClick={onDelete}
7585
tooltip={t(translations.deleteTooltip)}
76-
/>
86+
>
87+
{user.courses.length > 0 && (
88+
<>
89+
<PromptText>
90+
{t(translations.deletionPromptContent, {
91+
count: user.courses.length,
92+
})}
93+
</PromptText>
94+
<ol>
95+
{user.courses.map((course) => (
96+
<PromptText key={`course-${course.id}`}>
97+
<li>{course.title}</li>
98+
</PromptText>
99+
))}
100+
</ol>
101+
</>
102+
)}
103+
<PromptText>{t(translations.deletionConfirm)}</PromptText>
104+
</DeleteButton>
77105
</div>
78106
);
79107
};
80108

81-
export default memo(
82-
UserManagementButtons,
83-
(prevProps, nextProps) => {
84-
return equal(prevProps.user, nextProps.user);
85-
},
86-
);
109+
export default memo(UserManagementButtons, (prevProps, nextProps) => {
110+
return equal(prevProps.user, nextProps.user);
111+
});

client/app/bundles/system/admin/instance/instance/components/tables/UsersTable.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ import {
2828
import rebuildObjectFromRow from 'lib/helpers/mui-datatables-helpers';
2929
import { useAppDispatch } from 'lib/hooks/store';
3030
import toast from 'lib/hooks/toast';
31+
import useTranslation from 'lib/hooks/useTranslation';
3132
import tableTranslations from 'lib/translations/table';
3233

3334
import { indexUsers, updateUser } from '../../operations';
34-
import useTranslation from 'lib/hooks/useTranslation';
3535

3636
interface Props {
3737
users: InstanceUserMiniEntity[];
@@ -69,8 +69,7 @@ const translations = defineMessages({
6969
});
7070

7171
const UsersTable: FC<Props> = (props) => {
72-
const { title, renderRowActionComponent, users, userCounts, filter } =
73-
props;
72+
const { title, renderRowActionComponent, users, userCounts, filter } = props;
7473
const [isLoading, setIsLoading] = useState(false);
7574
const dispatch = useAppDispatch();
7675
const { t } = useTranslation();
@@ -123,9 +122,7 @@ const UsersTable: FC<Props> = (props) => {
123122
active: filter.active,
124123
}),
125124
)
126-
.catch(() =>
127-
toast.error(t(translations.fetchFilteredUsersFailure)),
128-
)
125+
.catch(() => toast.error(t(translations.fetchFilteredUsersFailure)))
129126
.finally(() => {
130127
setIsLoading(false);
131128
});
@@ -146,9 +143,7 @@ const UsersTable: FC<Props> = (props) => {
146143
search: searchText ? searchText.trim() : searchText,
147144
}),
148145
)
149-
.catch(() =>
150-
toast.error(t(translations.fetchFilteredUsersFailure)),
151-
)
146+
.catch(() => toast.error(t(translations.fetchFilteredUsersFailure)))
152147
.finally(() => {
153148
setIsLoading(false);
154149
});
@@ -256,7 +251,7 @@ const UsersTable: FC<Props> = (props) => {
256251
to={`/users/${user.userId}`}
257252
underline="hover"
258253
>
259-
{user.courses}
254+
{user.courses.length}
260255
</Link>
261256
);
262257
},

0 commit comments

Comments
 (0)