From 728fcab49da08497882fdba774fc8ac5fc59b75c Mon Sep 17 00:00:00 2001 From: OsamaSayegh Date: Wed, 20 Nov 2024 06:14:07 +0300 Subject: [PATCH] progress tracking and disable checkboxes for undeletable users --- .../bulk-user-delete-confirmation.gjs | 199 ++++++++++++++++++ .../controllers/admin-users-list-show.js | 73 +++---- .../addon/routes/admin-users-list-show.js | 3 +- .../admin/addon/templates/users-list-show.hbs | 59 ++++-- .../stylesheets/common/admin/admin_base.scss | 1 + .../admin/admin_bulk_users_delete_modal.scss | 23 ++ app/controllers/admin/users_controller.rb | 2 + app/serializers/admin_user_list_serializer.rb | 7 +- app/services/user/bulk_destroy.rb | 46 +++- config/locales/client.en.yml | 28 ++- spec/requests/admin/users_controller_spec.rb | 6 + spec/system/admin_users_list_spec.rb | 155 ++++++++++---- .../modals/bulk_user_delete_confirmation.rb | 66 ++++++ 13 files changed, 554 insertions(+), 114 deletions(-) create mode 100644 app/assets/javascripts/admin/addon/components/bulk-user-delete-confirmation.gjs create mode 100644 app/assets/stylesheets/common/admin/admin_bulk_users_delete_modal.scss create mode 100644 spec/system/page_objects/modals/bulk_user_delete_confirmation.rb diff --git a/app/assets/javascripts/admin/addon/components/bulk-user-delete-confirmation.gjs b/app/assets/javascripts/admin/addon/components/bulk-user-delete-confirmation.gjs new file mode 100644 index 00000000000..dbf17321829 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/bulk-user-delete-confirmation.gjs @@ -0,0 +1,199 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import { TrackedArray } from "@ember-compat/tracked-built-ins"; +import DButton from "discourse/components/d-button"; +import DModal from "discourse/components/d-modal"; +import { ajax } from "discourse/lib/ajax"; +import { extractError } from "discourse/lib/ajax-error"; +import { bind } from "discourse-common/utils/decorators"; +import { i18n } from "discourse-i18n"; + +const BULK_DELETE_CHANNEL = "/bulk-user-delete"; + +export default class BulkUserDeleteConfirmation extends Component { + @service messageBus; + + @tracked confirmButtonDisabled = true; + @tracked deleteStarted = false; + @tracked logs = new TrackedArray(); + failedUsernames = []; + + callAfterBulkDelete = false; + + constructor() { + super(...arguments); + this.messageBus.subscribe(BULK_DELETE_CHANNEL, this.onDeleteProgress); + } + + willDestroy() { + super.willDestroy(...arguments); + this.messageBus.unsubscribe(BULK_DELETE_CHANNEL, this.onDeleteProgress); + } + + get confirmDeletePhrase() { + return i18n( + "admin.users.bulk_actions.delete.confirmation_modal.confirmation_phrase", + { count: this.args.model.userIds.length } + ); + } + + #logError(line) { + this.#log(line, "error"); + } + + #logSuccess(line) { + this.#log(line, "success"); + } + + #logNeutral(line) { + this.#log(line, "neutral"); + } + + #log(line, type) { + this.logs.push({ + line, + type, + }); + } + + @bind + onDeleteProgress(data) { + if (data.success) { + this.#logSuccess( + i18n( + "admin.users.bulk_actions.delete.confirmation_modal.user_delete_succeeded", + { + position: data.position, + total: data.total, + username: data.username, + } + ) + ); + } else if (data.failed) { + this.failedUsernames.push(data.username); + this.#logError( + i18n( + "admin.users.bulk_actions.delete.confirmation_modal.user_delete_failed", + { + position: data.position, + total: data.total, + username: data.username, + error: data.error, + } + ) + ); + } + + if (data.position === data.total) { + this.callAfterBulkDelete = true; + this.#logNeutral( + i18n( + "admin.users.bulk_actions.delete.confirmation_modal.bulk_delete_finished" + ) + ); + if (this.failedUsernames.length > 0) { + this.#logNeutral( + i18n( + "admin.users.bulk_actions.delete.confirmation_modal.failed_to_delete_users" + ) + ); + for (const username of this.failedUsernames) { + this.#logNeutral(`* ${username}`); + } + } + } + } + + @action + onPromptInput(event) { + this.confirmButtonDisabled = + event.target.value.toLowerCase() !== this.confirmDeletePhrase; + } + + @action + async startDelete() { + this.deleteStarted = true; + this.confirmButtonDisabled = true; + this.#logNeutral( + i18n( + "admin.users.bulk_actions.delete.confirmation_modal.bulk_delete_starting" + ) + ); + + try { + await ajax("/admin/users/destroy-bulk.json", { + type: "DELETE", + data: { user_ids: this.args.model.userIds }, + }); + this.callAfterBulkDelete = true; + } catch (err) { + this.#logError(extractError(err)); + this.confirmButtonDisabled = false; + } + } + + @action + closeModal() { + this.args.closeModal(); + if (this.callAfterBulkDelete) { + this.args.model?.afterBulkDelete(); + } + } + + +} diff --git a/app/assets/javascripts/admin/addon/controllers/admin-users-list-show.js b/app/assets/javascripts/admin/addon/controllers/admin-users-list-show.js index 388a898a9a8..b9a3b9960ab 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-users-list-show.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-users-list-show.js @@ -2,25 +2,25 @@ import { tracked } from "@glimmer/tracking"; import Controller from "@ember/controller"; import { action } from "@ember/object"; import { service } from "@ember/service"; -import { observes } from "@ember-decorators/object"; import { computedI18n } from "discourse/lib/computed"; -import { ajax } from "discourse/lib/ajax"; -import { extractError } from "discourse/lib/ajax-error"; import CanCheckEmails from "discourse/mixins/can-check-emails"; import { INPUT_DELAY } from "discourse-common/config/environment"; import discourseDebounce from "discourse-common/lib/debounce"; -import discourseComputed from "discourse-common/utils/decorators"; +import discourseComputed, { bind } from "discourse-common/utils/decorators"; import { i18n } from "discourse-i18n"; +import BulkUserDeleteConfirmation from "admin/components/bulk-user-delete-confirmation"; import AdminUser from "admin/models/admin-user"; export default class AdminUsersListShowController extends Controller.extend( CanCheckEmails ) { @service dialog; + @service modal; @tracked bulkSelect = false; @tracked displayBulkActions = false; - @tracked bulkSelectedUsers = null; + @tracked bulkSelectedUserIdsSet = new Set(); + @tracked bulkSelectedUsersMap = {}; query = null; order = null; @@ -56,11 +56,6 @@ export default class AdminUsersListShowController extends Controller.extend( return colCount; } - @observes("listFilter") - _filterUsers() { - discourseDebounce(this, this.resetFilters, INPUT_DELAY); - } - resetFilters() { this._page = 1; this._results = []; @@ -96,6 +91,12 @@ export default class AdminUsersListShowController extends Controller.extend( }); } + @action + onListFilterChange(event) { + this.set("listFilter", event.target.value); + discourseDebounce(this, this.resetFilters, INPUT_DELAY); + } + @action loadMore() { this._page += 1; @@ -120,48 +121,36 @@ export default class AdminUsersListShowController extends Controller.extend( toggleBulkSelect() { this.bulkSelect = !this.bulkSelect; this.displayBulkActions = false; - this.bulkSelectedUsers = null; + this.bulkSelectedUsersMap = {}; + this.bulkSelectedUserIdsSet = new Set(); } @action bulkSelectItemToggle(userId, event) { - if (!this.bulkSelectedUsers) { - this.bulkSelectedUsers = {}; - } - if (event.target.checked) { - this.bulkSelectedUsers[userId] = 1; + this.bulkSelectedUserIdsSet.add(userId); + this.bulkSelectedUsersMap[userId] = 1; } else { - delete this.bulkSelectedUsers[userId]; + this.bulkSelectedUserIdsSet.delete(userId); + delete this.bulkSelectedUsersMap[userId]; } - this.displayBulkActions = Object.keys(this.bulkSelectedUsers).length > 0; + this.displayBulkActions = this.bulkSelectedUserIdsSet.size > 0; + } + + @bind + async afterBulkDelete() { + await this.resetFilters(); + this.bulkSelectedUsersMap = {}; + this.bulkSelectedUserIdsSet = new Set(); + this.displayBulkActions = false; } @action - performBulkDelete() { - const userIds = Object.keys(this.bulkSelectedUsers); - const count = userIds.length; - this.dialog.deleteConfirm({ - title: I18n.t("admin.users.bulk_actions.confirm_delete_title", { - count, - }), - message: I18n.t("admin.users.bulk_actions.confirm_delete_body", { - count, - }), - confirmButtonClass: "btn-danger", - confirmButtonIcon: "trash-can", - didConfirm: async () => { - try { - await ajax("/admin/users/destroy-bulk.json", { - type: "DELETE", - data: { user_ids: userIds }, - }); - await this.resetFilters(); - this.bulkSelectedUsers = null; - this.displayBulkActions = false; - } catch (err) { - this.dialog.alert(extractError(err)); - } + openBulkDeleteConfirmation() { + this.modal.show(BulkUserDeleteConfirmation, { + model: { + userIds: Array.from(this.bulkSelectedUserIdsSet), + afterBulkDelete: this.afterBulkDelete, }, }); } diff --git a/app/assets/javascripts/admin/addon/routes/admin-users-list-show.js b/app/assets/javascripts/admin/addon/routes/admin-users-list-show.js index 532fd150ad0..81970884e28 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-users-list-show.js +++ b/app/assets/javascripts/admin/addon/routes/admin-users-list-show.js @@ -24,7 +24,8 @@ export default class AdminUsersListShowRoute extends DiscourseRoute { listFilter: transition.to.queryParams.username, query: params.filter, refreshing: false, - bulkSelectedUsers: null, + bulkSelectedUsersMap: {}, + bulkSelectedUserIdsSet: new Set(), displayBulkActions: false, }); diff --git a/app/assets/javascripts/admin/addon/templates/users-list-show.hbs b/app/assets/javascripts/admin/addon/templates/users-list-show.hbs index 8795cbbc9eb..56160ae0738 100644 --- a/app/assets/javascripts/admin/addon/templates/users-list-show.hbs +++ b/app/assets/javascripts/admin/addon/templates/users-list-show.hbs @@ -21,10 +21,12 @@
-
{{#if this.displayBulkActions}} @@ -41,9 +43,11 @@ @@ -173,12 +177,38 @@ >
{{#if this.bulkSelect}} - + {{#if user.can_be_deleted}} + + {{else}} + + <:trigger> + + + <:content> + {{#if user.admin}} + {{i18n + "admin.users.bulk_actions.admin_cant_be_deleted" + }} + {{else}} + {{i18n + "admin.users.bulk_actions.too_many_or_old_posts" + }} + {{/if}} + + + {{/if}} {{/if}} - - - {{else}} + {{else if (not this.refreshing)}}

{{i18n "search.no_results"}}

{{/if}} + \ No newline at end of file diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 95f4b94eadc..324754fcdcd 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1105,3 +1105,4 @@ a.inline-editable-field { @import "common/admin/mini_profiler"; @import "common/admin/schema_theme_setting_editor"; @import "common/admin/customize_themes_show_schema"; +@import "common/admin/admin_bulk_users_delete_modal"; diff --git a/app/assets/stylesheets/common/admin/admin_bulk_users_delete_modal.scss b/app/assets/stylesheets/common/admin/admin_bulk_users_delete_modal.scss new file mode 100644 index 00000000000..4789926f4ba --- /dev/null +++ b/app/assets/stylesheets/common/admin/admin_bulk_users_delete_modal.scss @@ -0,0 +1,23 @@ +.bulk-user-delete-confirmation { + &__progress { + font-family: var(--d-font-family--monospace); + max-height: 400px; + background: var(--blend-primary-secondary-5); + padding: 1em; + overflow-y: auto; + } + &__progress-line { + overflow-anchor: none; + + &.-success { + color: var(--success); + } + &.-error { + color: var(--danger); + } + } + &__progress-anchor { + overflow-anchor: auto; + height: 1px; + } +} diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 66bac1c336e..29fa6f506eb 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -414,6 +414,8 @@ class Admin::UsersController < Admin::StaffController on_failed_policy(:can_delete_users) do render json: failed_json.merge(errors: [I18n.t("user.cannot_bulk_delete")]), status: 403 end + + on_model_not_found(:users) { render json: failed_json, status: 404 } end end end diff --git a/app/serializers/admin_user_list_serializer.rb b/app/serializers/admin_user_list_serializer.rb index 4ce713fb145..e705cc26e50 100644 --- a/app/serializers/admin_user_list_serializer.rb +++ b/app/serializers/admin_user_list_serializer.rb @@ -24,7 +24,8 @@ class AdminUserListSerializer < BasicUserSerializer :silenced_till, :time_read, :staged, - :second_factor_enabled + :second_factor_enabled, + :can_be_deleted %i[days_visited posts_read_count topics_entered post_count].each do |sym| attributes sym @@ -111,4 +112,8 @@ class AdminUserListSerializer < BasicUserSerializer def second_factor_enabled true end + + def can_be_deleted + scope.can_delete_user?(object) + end end diff --git a/app/services/user/bulk_destroy.rb b/app/services/user/bulk_destroy.rb index 775284c2374..ca36ce63b24 100644 --- a/app/services/user/bulk_destroy.rb +++ b/app/services/user/bulk_destroy.rb @@ -16,7 +16,11 @@ class User::BulkDestroy private def fetch_users(params:) - User.where(id: params.user_ids.to_a) + ids = params.user_ids.to_a + # this order cluase ensures we retrieve the users in the same order as the + # IDs in the param. we do this to ensure the users are deleted in the same + # order as they're selected in the UI + User.where(id: ids).order(DB.sql_fragment("array_position(ARRAY[?], users.id)", ids)) end def can_delete_users(guardian:, users:) @@ -24,12 +28,42 @@ class User::BulkDestroy end def delete(users:, guardian:) - users.each do |u| - UserDestroyer.new(guardian.user).destroy( - u, - delete_posts: true, - context: I18n.t("staff_action_logs.bulk_user_delete", users: users.map(&:id).inspect), + users.each.with_index do |u, index| + position = index + 1 + success = + UserDestroyer.new(guardian.user).destroy( + u, + delete_posts: true, + prepare_for_destroy: true, + context: I18n.t("staff_action_logs.bulk_user_delete", users: users.map(&:id).inspect), + ) + + if success + publish_progress( + guardian.user, + { position:, username: u.username, total: users.size, success: true }, + ) + else + publish_progress( + guardian.user, + { + position:, + username: u.username, + total: users.size, + failed: true, + error: u.errors.full_messages.join(", "), + }, + ) + end + rescue => err + publish_progress( + guardian.user, + { position:, username: u.username, total: users.size, failed: true, error: err.message }, ) end end + + def publish_progress(actor, data) + ::MessageBus.publish("/bulk-user-delete", data, user_ids: [actor.id]) + end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 523cc0c4fb8..994a758157e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -6641,13 +6641,27 @@ en: hide_emails: "Hide Emails" bulk_actions: title: "Bulk actions" - delete: "Delete users…" - confirm_delete_title: - one: "Delete %{count} user?" - other: "Delete %{count} users?" - confirm_delete_body: - one: "Are you sure you want to delete %{count} user? This action is permanent and cannot be reversed." - other: "Are you sure you want to delete %{count} users? This action is permanent and cannot be reversed." + admin_cant_be_deleted: "This user can't be deleted because they're an admin" + too_many_or_old_posts: "This user can't be deleted they have too many posts or a very old post" + delete: + label: "Delete users…" + confirmation_modal: + prompt_text: + one: "You're about to delete %{count} user permanently. Type \"%{confirmation_phrase}\" below to proceed:" + other: "You're about to delete %{count} users permanently. Type \"%{confirmation_phrase}\" below to proceed:" + confirmation_phrase: + one: "delete %{count} user" + other: "delete %{count} users" + close: "Close" + confirm: "Delete" + title: + one: "Delete %{count} user" + other: "Delete %{count} users" + bulk_delete_starting: "Starting bulk delete…" + user_delete_succeeded: "[%{position}/%{total}] Successfully deleted @%{username}" + user_delete_failed: "[%{position}/%{total}] Failed to delete @%{username} - %{error}" + bulk_delete_finished: "Bulk delete operation completed." + failed_to_delete_users: "The following users failed to be deleted:" nav: new: "New" active: "Active" diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb index 81ab125741e..6803524db48 100644 --- a/spec/requests/admin/users_controller_spec.rb +++ b/spec/requests/admin/users_controller_spec.rb @@ -1445,6 +1445,12 @@ RSpec.describe Admin::UsersController do expect(User.where(id: deleted_users.map(&:id)).count).to eq(0) end + it "responds with 404 when sending an empty user_ids list" do + delete "/admin/users/destroy-bulk.json", params: { user_ids: [] } + + expect(response.status).to eq(404) + end + it "doesn't allow deleting a user that can't be deleted" do deleted_users[0].update!(admin: true) diff --git a/spec/system/admin_users_list_spec.rb b/spec/system/admin_users_list_spec.rb index bc571946804..e92634a597a 100644 --- a/spec/system/admin_users_list_spec.rb +++ b/spec/system/admin_users_list_spec.rb @@ -2,70 +2,141 @@ describe "Admin Users Page", type: :system do fab!(:current_user) { Fabricate(:admin) } + fab!(:another_admin) { Fabricate(:admin) } fab!(:users) { Fabricate.times(3, :user) } let(:admin_users_page) { PageObjects::Pages::AdminUsers.new } - let(:dialog) { PageObjects::Components::Dialog.new } before { sign_in(current_user) } - it "has a button that toggles the bulk select checkboxes" do - admin_users_page.visit + describe "bulk user delete" do + let(:confirmation_modal) { PageObjects::Modals::BulkUserDeleteConfirmation.new } - expect(admin_users_page).to have_users(users.map(&:id)) + it "disables checkboxes for users that can't be deleted" do + admin_users_page.visit - expect(admin_users_page.user_row(users[0].id)).to have_no_bulk_select_checkbox - expect(admin_users_page.user_row(users[1].id)).to have_no_bulk_select_checkbox - expect(admin_users_page.user_row(users[2].id)).to have_no_bulk_select_checkbox + admin_users_page.bulk_select_button.click - admin_users_page.bulk_select_button.click + expect(admin_users_page.user_row(current_user.id).bulk_select_checkbox.disabled?).to eq(true) + expect(admin_users_page.user_row(another_admin.id).bulk_select_checkbox.disabled?).to eq(true) + expect(admin_users_page.user_row(users[0].id).bulk_select_checkbox.disabled?).to eq(false) - expect(admin_users_page.user_row(users[0].id)).to have_bulk_select_checkbox - expect(admin_users_page.user_row(users[1].id)).to have_bulk_select_checkbox - expect(admin_users_page.user_row(users[2].id)).to have_bulk_select_checkbox + admin_users_page.user_row(another_admin.id).bulk_select_checkbox.hover + expect(PageObjects::Components::Tooltips.new("bulk-delete-unavailable-reason")).to be_present( + text: I18n.t("admin_js.admin.users.bulk_actions.admin_cant_be_deleted"), + ) + end - expect(admin_users_page).to have_no_bulk_actions_dropdown + it "has a button that toggles the bulk select checkboxes" do + admin_users_page.visit - admin_users_page.user_row(users[0].id).bulk_select_checkbox.click + expect(admin_users_page).to have_users(users.map(&:id)) - expect(admin_users_page).to have_bulk_actions_dropdown + expect(admin_users_page.user_row(users[0].id)).to have_no_bulk_select_checkbox + expect(admin_users_page.user_row(users[1].id)).to have_no_bulk_select_checkbox + expect(admin_users_page.user_row(users[2].id)).to have_no_bulk_select_checkbox - admin_users_page.user_row(users[1].id).bulk_select_checkbox.click - admin_users_page.bulk_actions_dropdown.expand - admin_users_page.bulk_actions_dropdown.option(".bulk-delete").click + admin_users_page.bulk_select_button.click - expect(dialog).to be_open - dialog.click_danger + expect(admin_users_page.user_row(users[0].id)).to have_bulk_select_checkbox + expect(admin_users_page.user_row(users[1].id)).to have_bulk_select_checkbox + expect(admin_users_page.user_row(users[2].id)).to have_bulk_select_checkbox - deleted_ids = users[0..1].map(&:id) - expect(admin_users_page).to have_no_users(deleted_ids) - expect(User.where(id: deleted_ids).count).to eq(0) - end + expect(admin_users_page).to have_no_bulk_actions_dropdown - it "remembers selected users when the user list refreshes due to search" do - admin_users_page.visit - admin_users_page.bulk_select_button.click - admin_users_page.search_input.fill_in(with: users[0].username) - admin_users_page.user_row(users[0].id).bulk_select_checkbox.click + admin_users_page.user_row(users[0].id).bulk_select_checkbox.click - admin_users_page.search_input.fill_in(with: users[1].username) - admin_users_page.user_row(users[1].id).bulk_select_checkbox.click + expect(admin_users_page).to have_bulk_actions_dropdown - admin_users_page.search_input.fill_in(with: "") + admin_users_page.user_row(users[1].id).bulk_select_checkbox.click + admin_users_page.bulk_actions_dropdown.expand + admin_users_page.bulk_actions_dropdown.option(".bulk-delete").click - expect(admin_users_page).to have_users(users.map(&:id)) - expect(admin_users_page.user_row(users[0].id).bulk_select_checkbox).to be_checked - expect(admin_users_page.user_row(users[1].id).bulk_select_checkbox).to be_checked - expect(admin_users_page.user_row(users[2].id).bulk_select_checkbox).not_to be_checked + expect(confirmation_modal).to be_open + expect(confirmation_modal).to have_confirm_button_disabled - admin_users_page.bulk_actions_dropdown.expand - admin_users_page.bulk_actions_dropdown.option(".bulk-delete").click + confirmation_modal.fill_in_confirmation_phase(user_count: 3) + expect(confirmation_modal).to have_confirm_button_disabled - expect(dialog).to be_open - dialog.click_danger + confirmation_modal.fill_in_confirmation_phase(user_count: 2) + expect(confirmation_modal).to have_confirm_button_enabled - deleted_ids = users[0..1].map(&:id) - expect(admin_users_page).to have_no_users(deleted_ids) - expect(User.where(id: deleted_ids).count).to eq(0) + confirmation_modal.confirm_button.click + + expect(confirmation_modal).to have_successful_log_entry_for_user( + user: users[0], + position: 1, + total: 2, + ) + expect(confirmation_modal).to have_successful_log_entry_for_user( + user: users[1], + position: 2, + total: 2, + ) + expect(confirmation_modal).to have_no_error_log_entries + + confirmation_modal.close + deleted_ids = users[0..1].map(&:id) + expect(admin_users_page).to have_no_users(deleted_ids) + expect(User.where(id: deleted_ids).count).to eq(0) + end + + it "remembers selected users when the user list refreshes due to search" do + admin_users_page.visit + admin_users_page.bulk_select_button.click + admin_users_page.search_input.fill_in(with: users[0].username) + admin_users_page.user_row(users[0].id).bulk_select_checkbox.click + + admin_users_page.search_input.fill_in(with: users[1].username) + admin_users_page.user_row(users[1].id).bulk_select_checkbox.click + + admin_users_page.search_input.fill_in(with: "") + + expect(admin_users_page).to have_users(users.map(&:id)) + expect(admin_users_page.user_row(users[0].id).bulk_select_checkbox).to be_checked + expect(admin_users_page.user_row(users[1].id).bulk_select_checkbox).to be_checked + expect(admin_users_page.user_row(users[2].id).bulk_select_checkbox).not_to be_checked + + admin_users_page.bulk_actions_dropdown.expand + admin_users_page.bulk_actions_dropdown.option(".bulk-delete").click + + expect(confirmation_modal).to be_open + confirmation_modal.fill_in_confirmation_phase(user_count: 2) + confirmation_modal.confirm_button.click + expect(confirmation_modal).to have_successful_log_entry_for_user( + user: users[0], + position: 1, + total: 2, + ) + expect(confirmation_modal).to have_successful_log_entry_for_user( + user: users[1], + position: 2, + total: 2, + ) + confirmation_modal.close + + deleted_ids = users[0..1].map(&:id) + expect(admin_users_page).to have_no_users(deleted_ids) + expect(User.where(id: deleted_ids).count).to eq(0) + end + + it "displays an error message if bulk delete fails" do + admin_users_page.visit + admin_users_page.bulk_select_button.click + + admin_users_page.user_row(users[0].id).bulk_select_checkbox.click + admin_users_page.bulk_actions_dropdown.expand + admin_users_page.bulk_actions_dropdown.option(".bulk-delete").click + confirmation_modal.fill_in_confirmation_phase(user_count: 1) + + users[0].update!(admin: true) + + confirmation_modal.confirm_button.click + expect(confirmation_modal).to have_error_log_entry( + I18n.t("js.generic_error_with_reason", error: I18n.t("user.cannot_bulk_delete")), + ) + confirmation_modal.close + expect(admin_users_page).to have_users([users[0].id]) + end end end diff --git a/spec/system/page_objects/modals/bulk_user_delete_confirmation.rb b/spec/system/page_objects/modals/bulk_user_delete_confirmation.rb new file mode 100644 index 00000000000..f70b93b1a3f --- /dev/null +++ b/spec/system/page_objects/modals/bulk_user_delete_confirmation.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module PageObjects + module Modals + class BulkUserDeleteConfirmation < Base + MODAL_SELECTOR = ".bulk-user-delete-confirmation" + + def confirm_button + within(modal) { find(".btn.confirm-delete") } + end + + def has_confirm_button_disabled? + within(modal) { has_css?(".btn.confirm-delete[disabled]") } + end + + def has_confirm_button_enabled? + within(modal) do + has_no_css?(".btn.confirm-delete[disabled]") && has_css?(".btn.confirm-delete") + end + end + + def fill_in_confirmation_phase(user_count:) + within(modal) do + find("input.confirmation-phrase").fill_in( + with: + I18n.t( + "admin_js.admin.users.bulk_actions.delete.confirmation_modal.confirmation_phrase", + count: user_count, + ), + ) + end + end + + def has_successful_log_entry_for_user?(user:, position:, total:) + within(modal) do + has_css?( + ".bulk-user-delete-confirmation__progress-line.-success", + text: + I18n.t( + "admin_js.admin.users.bulk_actions.delete.confirmation_modal.user_delete_succeeded", + position:, + total:, + username: user.username, + ), + ) + end + end + + def has_no_error_log_entries? + within(modal) { has_no_css?(".bulk-user-delete-confirmation__progress-line.-error") } + end + + def has_error_log_entry?(message) + within(modal) do + has_css?(".bulk-user-delete-confirmation__progress-line.-error", text: message) + end + end + + private + + def modal + find(MODAL_SELECTOR) + end + end + end +end