discourse/app/assets/javascripts/admin/addon/controllers/admin-users-list-show.js
Joffrey JAFFEUX b6aad28ccf
DEV: replace selenium driver with playwright (#31977)
This commit is replacing the system specs driver (selenium) by
Playwright: https://playwright.dev/

We are still using Capybara to write the specs but they will now be run
by Playwright. To achieve this we are using the non official ruby
driver: https://github.com/YusukeIwaki/capybara-playwright-driver

### Notable changes

- `CHROME_DEV_TOOLS` has been removed, it's not working well with
playwright use `pause_test` and inspect browser for now.

- `fill_in` is not generating key events in playwright, use `send_keys`
if you need this.

### New spec options

#### trace

Allows to capture a trace in a zip file which you can load at
https://trace.playwright.dev or locally through `npx playwright
show-trace /path/to/trace.zip`

_Example usage:_

```ruby
it "shows bar", trace: true do
  visit("/")

  find(".foo").click

  expect(page).to have_css(".bar")
end
```

#### video

Allows to capture a video of your spec.

_Example usage:_

```ruby
it "shows bar", video: true do
  visit("/")

  find(".foo").click

  expect(page).to have_css(".bar")
end
```

### New env variable

#### PLAYWRIGHT_SLOW_MO_MS

Allow to force playwright to wait DURATION (in ms) at each action.

_Example usage:_

```
PLAYWRIGHT_SLOW_MO_MS=1000 rspec foo_spec.rb
```

#### PLAYWRIGHT_HEADLESS

Allow to be in headless mode or not. Default will be headless.

_Example usage:_

```
PLAYWRIGHT_HEADLESS=0 rspec foo_spec.rb # will show the browser
```

### New helpers

#### with_logs

Allows to access the browser logs and check if something specific has
been logged.

_Example usage:_

```ruby
with_logs do |logger|
  # do something

  expect(logger.logs.map { |log| log[:message] }).to include("foo")
end
```

#### add_cookie

Allows to add a cookie on the browser session.

_Example usage:_

```ruby
add_cookie(name: "destination_url", value: "/new")
```

#### get_style

Get the property style value of an element.

_Example usage:_

```ruby
expect(get_style(find(".foo"), "height")).to eq("200px")
```

#### get_rgb_color

Get the rgb color of an element.

_Example usage:_

```ruby
expect(get_rgb_color(find("html"), "backgroundColor")).to eq("rgb(170, 51, 159)")
```
2025-05-06 10:44:14 +02:00

249 lines
6.2 KiB
JavaScript
Vendored

import { tracked } from "@glimmer/tracking";
import Controller from "@ember/controller";
import { action, computed } from "@ember/object";
import { service } from "@ember/service";
import { TrackedArray } from "@ember-compat/tracked-built-ins";
import CanCheckEmailsHelper from "discourse/lib/can-check-emails-helper";
import { computedI18n, setting } from "discourse/lib/computed";
import discourseDebounce from "discourse/lib/debounce";
import discourseComputed, { bind } from "discourse/lib/decorators";
import { INPUT_DELAY } from "discourse/lib/environment";
import { i18n } from "discourse-i18n";
import BulkUserDeleteConfirmation from "admin/components/bulk-user-delete-confirmation";
import AdminUser from "admin/models/admin-user";
const MAX_BULK_SELECT_LIMIT = 100;
export default class AdminUsersListShowController extends Controller {
@service dialog;
@service modal;
@service toasts;
@tracked bulkSelect = false;
@tracked displayBulkActions = false;
@tracked bulkSelectedUserIdsSet = new Set();
@tracked bulkSelectedUsersMap = {};
@setting("moderators_view_emails") canModeratorsViewEmails;
query = null;
order = null;
asc = null;
showEmails = false;
refreshing = false;
listFilter = null;
lastSelected = null;
lastBulkDeleteMessageBusId = null;
@computedI18n("search_hint") searchHint;
_page = 1;
_results = new TrackedArray();
_canLoadMore = true;
get users() {
return this._results.flat();
}
@discourseComputed("query")
title(query) {
return i18n("admin.users.titles." + query);
}
@discourseComputed("showEmails")
columnCount(showEmails) {
let colCount = 7; // note that the first column is hardcoded in the template
if (showEmails) {
colCount += 1;
}
if (this.siteSettings.must_approve_users) {
colCount += 1;
}
return colCount;
}
@computed("model.id", "currentUser.id")
get canCheckEmails() {
return new CanCheckEmailsHelper(
this.model?.id,
this.canModeratorsViewEmails,
this.currentUser
).canCheckEmails;
}
@computed("model.id", "currentUser.id")
get canAdminCheckEmails() {
return new CanCheckEmailsHelper(
this.model?.id,
this.canModeratorsViewEmails,
this.currentUser
).canAdminCheckEmails;
}
@computed("query")
get showSilenceReason() {
return this.query === "silenced";
}
resetFilters() {
this._page = 1;
this._results.length = 0;
this._canLoadMore = true;
return this._refreshUsers();
}
stripHtml(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
return doc.body.textContent || "";
}
_refreshUsers() {
if (!this._canLoadMore) {
return;
}
const page = this._page;
this.set("refreshing", true);
return AdminUser.findAll(this.query, {
filter: this.listFilter,
show_emails: this.showEmails,
order: this.order,
asc: this.asc,
page,
})
.then((result) => {
this.lastBulkDeleteMessageBusId =
result.meta?.message_bus_last_ids?.bulk_delete;
this._results[page] = result.users;
if (result.users.length === 0) {
this._canLoadMore = false;
}
})
.finally(() => {
this.set("refreshing", false);
});
}
@action
onListFilterChange(event) {
this.set("listFilter", event.target.value);
discourseDebounce(this, this.resetFilters, INPUT_DELAY);
}
@action
loadMore() {
if (this.refreshing) {
return;
}
this._page += 1;
this._refreshUsers();
}
@action
toggleEmailVisibility() {
this.toggleProperty("showEmails");
this.resetFilters();
}
@action
updateOrder(field, asc) {
this.setProperties({
order: field,
asc,
});
}
@action
toggleBulkSelect() {
this.bulkSelect = !this.bulkSelect;
this.displayBulkActions = false;
this.bulkSelectedUsersMap = {};
this.bulkSelectedUserIdsSet = new Set();
}
@action
bulkSelectItemToggle(userId, event) {
if (event.target.checked) {
if (!this.#canBulkSelectMoreUsers(1)) {
this.#showBulkSelectionLimitToast(event);
return;
}
if (event.shiftKey && this.lastSelected) {
const list = Array.from(
document.querySelectorAll(
"input.directory-table__cell-bulk-select:not([disabled])"
)
);
const lastSelectedIndex = list.indexOf(this.lastSelected);
if (lastSelectedIndex !== -1) {
const newSelectedIndex = list.indexOf(event.target);
const start = Math.min(lastSelectedIndex, newSelectedIndex);
const end = Math.max(lastSelectedIndex, newSelectedIndex);
if (!this.#canBulkSelectMoreUsers(end - start)) {
this.#showBulkSelectionLimitToast(event);
return;
}
list.slice(start, end).forEach((input) => {
input.checked = true;
this.#addUserToBulkSelection(parseInt(input.dataset.userId, 10));
});
}
}
this.#addUserToBulkSelection(userId);
this.lastSelected = event.target;
} else {
this.bulkSelectedUserIdsSet.delete(userId);
delete this.bulkSelectedUsersMap[userId];
}
this.displayBulkActions = this.bulkSelectedUserIdsSet.size > 0;
}
@bind
async afterBulkDelete() {
await this.resetFilters();
this.bulkSelectedUsersMap = {};
this.bulkSelectedUserIdsSet = new Set();
this.displayBulkActions = false;
}
@action
openBulkDeleteConfirmation() {
this.modal.show(BulkUserDeleteConfirmation, {
model: {
lastBulkDeleteMessageBusId: this.lastBulkDeleteMessageBusId,
userIds: Array.from(this.bulkSelectedUserIdsSet),
afterBulkDelete: this.afterBulkDelete,
},
});
}
#addUserToBulkSelection(userId) {
this.bulkSelectedUserIdsSet.add(userId);
this.bulkSelectedUsersMap[userId] = 1;
}
#canBulkSelectMoreUsers(count) {
return this.bulkSelectedUserIdsSet.size + count <= MAX_BULK_SELECT_LIMIT;
}
#showBulkSelectionLimitToast(event) {
this.toasts.error({
duration: 3000,
data: {
message: i18n("admin.users.bulk_actions.too_many_selected_users", {
count: MAX_BULK_SELECT_LIMIT,
}),
},
});
event.preventDefault();
}
}