Extract moderator tools functionality and put it in a component

This makes it easier for 3rd parties to embed it in other modals (besides the bootstrap modal).
This commit is contained in:
JC Brand 2021-07-27 12:01:12 +02:00
parent 10c610232c
commit 0242fdb020
11 changed files with 385 additions and 294 deletions

View File

@ -2,10 +2,11 @@
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import { AFFILIATIONS } from '@converse/headless/plugins/muc/index.js';
import difference from 'lodash-es/difference';
import indexOf from 'lodash-es/indexOf';
import log from "@converse/headless/log";
import { api, converse } from '@converse/headless/core.js';
import { _converse, api, converse } from '@converse/headless/core.js';
import { parseMemberListIQ } from '../parsers.js';
const { Strophe, $iq, u } = converse.env;
@ -20,19 +21,20 @@ const { Strophe, $iq, u } = converse.env;
* @returns { Promise<MemberListItem[]> }
*/
export async function getAffiliationList (affiliation, muc_jid) {
const { __ } = _converse;
const iq = $iq({ 'to': muc_jid, 'type': 'get' })
.c('query', { xmlns: Strophe.NS.MUC_ADMIN })
.c('item', { 'affiliation': affiliation });
const result = await api.sendIQ(iq, null, false);
if (result === null) {
const err_msg = `Error: timeout while fetching ${affiliation} list for MUC ${muc_jid}`;
const err_msg = __('Error: timeout while fetching %1s list for MUC %2s', affiliation, muc_jid);
const err = new Error(err_msg);
log.warn(err_msg);
log.warn(result);
return err;
}
if (u.isErrorStanza(result)) {
const err_msg = `Error: not allowed to fetch ${affiliation} list for MUC ${muc_jid}`;
const err_msg = __('Error: not allowed to fetch %1s list for MUC %2s', affiliation, muc_jid);
const err = new Error(err_msg);
log.warn(err_msg);
log.warn(result);
@ -43,6 +45,28 @@ export async function getAffiliationList (affiliation, muc_jid) {
.sort((a, b) => (a.nick < b.nick ? -1 : a.nick > b.nick ? 1 : 0));
}
/**
* Given an occupant model, see which affiliations may be assigned to that user.
* @param { Model } occupant
* @returns { ('owner', 'admin', 'member', 'outcast', 'none')[] } - An array of assignable affiliations
*/
export function getAssignableAffiliations (occupant) {
let disabled = api.settings.get('modtools_disable_assign');
if (!Array.isArray(disabled)) {
disabled = disabled ? AFFILIATIONS : [];
}
if (occupant.get('affiliation') === 'owner') {
return AFFILIATIONS.filter(a => !disabled.includes(a));
} else if (occupant.get('affiliation') === 'admin') {
return AFFILIATIONS.filter(a => !['owner', 'admin', ...disabled].includes(a));
} else {
return [];
}
}
// Necessary for tests
_converse.getAssignableAffiliations = getAssignableAffiliations;
/**
* Send IQ stanzas to the server to modify affiliations for users in this groupchat.
* See: https://xmpp.org/extensions/xep-0045.html#modifymember

View File

@ -236,6 +236,7 @@ converse.plugins.add('converse-muc', {
'auto_register_muc_nickname': false,
'hide_muc_participants': false,
'locked_muc_domain': false,
'modtools_disable_assign': false,
'muc_clear_messages_on_leave': true,
'muc_domain': undefined,
'muc_fetch_members': true,

View File

@ -0,0 +1,21 @@
import { ROLES } from '@converse/headless/plugins/muc/index.js';
import { _converse, api } from '@converse/headless/core.js';
/**
* Given an occupant model, see which roles may be assigned to that user.
* @param { Model } occupant
* @returns { ('moderator', 'participant', 'visitor')[] } - An array of assignable roles
*/
export function getAssignableRoles (occupant) {
let disabled = api.settings.get('modtools_disable_assign');
if (!Array.isArray(disabled)) {
disabled = disabled ? ROLES : [];
}
if (occupant.get('role') === 'moderator') {
return ROLES.filter(r => !disabled.includes(r));
} else {
return [];
}
}
Object.assign(_converse, { getAssignableRoles });

View File

@ -4,7 +4,7 @@ import { CustomElement } from 'shared/components/element.js';
import { __ } from 'i18n';
import { _converse, api } from "@converse/headless/core";
import './styles//chat-head.scss';
import './styles/chat-head.scss';
export default class ChatHeading extends CustomElement {

View File

@ -44,7 +44,6 @@ converse.plugins.add('converse-muc-views', {
'cache_muc_messages': true,
'locked_muc_nickname': false,
'modtools_disable_query': [],
'modtools_disable_assign': false,
'muc_disable_slash_commands': false,
'muc_mention_autocomplete_filter': 'contains',
'muc_mention_autocomplete_min_chars': 0,

View File

@ -1,14 +1,6 @@
import '../modtools.js';
import BootstrapModal from "modals/base.js";
import log from "@converse/headless/log";
import tpl_moderator_tools_modal from "../templates/moderator-tools.js";
import { AFFILIATIONS, ROLES } from "@converse/headless/plugins/muc/index.js";
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { getAffiliationList, setAffiliation } from '@converse/headless/plugins/muc/affiliations/utils.js'
const { Strophe, sizzle } = converse.env;
const u = converse.env.utils;
import tpl_moderator_tools from './templates/moderator-tools.js';
const ModeratorToolsModal = BootstrapModal.extend({
id: "converse-modtools-modal",
@ -17,190 +9,10 @@ const ModeratorToolsModal = BootstrapModal.extend({
initialize (attrs) {
this.muc = attrs.muc;
BootstrapModal.prototype.initialize.apply(this, arguments);
this.affiliations_filter = '';
this.roles_filter = '';
this.listenTo(this.model, 'change:role', () => {
this.users_with_role = this.muc.getOccupantsWithRole(this.model.get('role'));
this.render();
});
this.listenTo(this.model, 'change:affiliation', async () => {
this.loading_users_with_affiliation = true;
this.users_with_affiliation = null;
this.render();
const affiliation = this.model.get('affiliation');
if (this.shouldFetchAffiliationsList()) {
const muc_jid = this.muc.get('jid');
this.users_with_affiliation = await getAffiliationList(affiliation, muc_jid);
} else {
this.users_with_affiliation = this.muc.getOccupantsWithAffiliation(affiliation);
}
this.loading_users_with_affiliation = false;
this.render();
});
},
toHTML () {
const occupant = this.muc.occupants.findWhere({'jid': _converse.bare_jid});
return tpl_moderator_tools_modal(Object.assign(this.model.toJSON(), {
'affiliations_filter': this.affiliations_filter,
'assignAffiliation': ev => this.assignAffiliation(ev),
'assignRole': ev => this.assignRole(ev),
'assignable_affiliations': this.getAssignableAffiliations(occupant),
'assignable_roles': this.getAssignableRoles(occupant),
'filterAffiliationResults': ev => this.filterAffiliationResults(ev),
'filterRoleResults': ev => this.filterRoleResults(ev),
'loading_users_with_affiliation': this.loading_users_with_affiliation,
'queryAffiliation': ev => this.queryAffiliation(ev),
'queryRole': ev => this.queryRole(ev),
'queryable_affiliations': AFFILIATIONS.filter(a => !_converse.modtools_disable_query.includes(a)),
'queryable_roles': ROLES.filter(a => !_converse.modtools_disable_query.includes(a)),
'roles_filter': this.roles_filter,
'switchTab': ev => this.switchTab(ev),
'toggleForm': ev => this.toggleForm(ev),
'users_with_affiliation': this.users_with_affiliation,
'users_with_role': this.users_with_role
}));
},
getAssignableAffiliations (occupant) {
let disabled = api.settings.get('modtools_disable_assign');
if (!Array.isArray(disabled)) {
disabled = disabled ? AFFILIATIONS : [];
}
if (occupant.get('affiliation') === 'owner') {
return AFFILIATIONS.filter(a => !disabled.includes(a));
} else if (occupant.get('affiliation') === 'admin') {
return AFFILIATIONS.filter(a => !['owner', 'admin', ...disabled].includes(a));
} else {
return [];
}
},
getAssignableRoles (occupant) {
let disabled = api.settings.get('modtools_disable_assign');
if (!Array.isArray(disabled)) {
disabled = disabled ? ROLES : [];
}
if (occupant.get('role') === 'moderator') {
return ROLES.filter(r => !disabled.includes(r));
} else {
return [];
}
},
shouldFetchAffiliationsList () {
const affiliation = this.model.get('affiliation');
if (affiliation === 'none') {
return false;
}
const chatroom = this.muc;
const auto_fetched_affs = chatroom.occupants.getAutoFetchedAffiliationLists();
if (auto_fetched_affs.includes(affiliation)) {
return false;
} else {
return true;
}
},
toggleForm (ev) {
ev.stopPropagation();
ev.preventDefault();
const form_class = ev.target.getAttribute('data-form');
const form = u.ancestor(ev.target, '.list-group-item').querySelector(`.${form_class}`);
if (u.hasClass('hidden', form)) {
u.removeClass('hidden', form);
} else {
u.addClass('hidden', form);
}
},
filterRoleResults (ev) {
this.roles_filter = ev.target.value;
this.render();
},
filterAffiliationResults (ev) {
this.affiliations_filter = ev.target.value;
this.render();
},
queryRole (ev) {
ev.stopPropagation();
ev.preventDefault();
const data = new FormData(ev.target);
const role = data.get('role');
this.model.set({'role': null}, {'silent': true});
this.model.set({'role': role});
},
queryAffiliation (ev) {
ev.stopPropagation();
ev.preventDefault();
const data = new FormData(ev.target);
const affiliation = data.get('affiliation');
this.model.set({'affiliation': null}, {'silent': true});
this.model.set({'affiliation': affiliation});
},
async assignAffiliation (ev) {
ev.stopPropagation();
ev.preventDefault();
const data = new FormData(ev.target);
const affiliation = data.get('affiliation');
const attrs = {
'jid': data.get('jid'),
'reason': data.get('reason')
}
const current_affiliation = this.model.get('affiliation');
const muc_jid = this.muc.get('jid');
try {
await setAffiliation(affiliation, muc_jid, [attrs]);
} catch (e) {
if (e === null) {
this.alert(__('Timeout error while trying to set the affiliation'), 'danger');
} else if (sizzle(`not-allowed[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
this.alert(__('Sorry, you\'re not allowed to make that change'), 'danger');
} else {
this.alert(__('Sorry, something went wrong while trying to set the affiliation'), 'danger');
}
log.error(e);
return;
}
this.alert(__('Affiliation changed'), 'primary');
await this.muc.occupants.fetchMembers()
this.model.set({'affiliation': null}, {'silent': true});
this.model.set({'affiliation': current_affiliation});
},
assignRole (ev) {
ev.stopPropagation();
ev.preventDefault();
const data = new FormData(ev.target);
const occupant = this.muc.getOccupant(data.get('jid') || data.get('nick'));
const role = data.get('role');
const reason = data.get('reason');
const current_role = this.model.get('role');
this.muc.setRole(occupant, role, reason,
() => {
this.alert(__('Role changed'), 'primary');
this.model.set({'role': null}, {'silent': true});
this.model.set({'role': current_role});
},
(e) => {
if (sizzle(`not-allowed[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
this.alert(__('You\'re not allowed to make that change'), 'danger');
} else {
this.alert(__('Sorry, something went wrong while trying to set the role'), 'danger');
if (u.isErrorObject(e)) {
log.error(e);
}
}
}
);
return tpl_moderator_tools(this);
}
});

View File

@ -0,0 +1,19 @@
import { __ } from 'i18n';
import { html } from "lit";
import { modal_header_close_button } from "modals/templates/buttons.js"
export default (o) => {
const i18n_moderator_tools = __('Moderator Tools');
return html`
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="converse-modtools-modal-label">${i18n_moderator_tools}</h5>
${modal_header_close_button}
</div>
<div class="modal-body d-flex flex-column">
<converse-modtools .muc=${o.muc} .model=${o.model}></converse-modtools>
</div>
</div>
</div>`;
}

View File

@ -0,0 +1,225 @@
import log from '@converse/headless/log';
import tpl_moderator_tools from './templates/moderator-tools.js';
import { AFFILIATIONS, ROLES } from '@converse/headless/plugins/muc/index.js';
import { CustomElement } from 'shared/components/element.js';
import { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core';
import { getAssignableRoles } from '@converse/headless/plugins/muc/utils.js';
import {
getAffiliationList,
getAssignableAffiliations,
setAffiliation,
} from '@converse/headless/plugins/muc/affiliations/utils.js';
const { Strophe, sizzle, u } = converse.env;
export default class ModeratorTools extends CustomElement {
static get properties () {
return {
affiliations_filter: { type: String, attribute: false },
alert_message: { type: String, attribute: false },
alert_type: { type: String, attribute: false },
model: { type: Object },
muc: { type: Object },
roles_filter: { type: String, attribute: false },
users_with_affiliation: { type: Array, attribute: false },
users_with_role: { type: Array, attribute: false },
};
}
constructor () {
super();
this.affiliations_filter = '';
this.roles_filter = '';
}
connectedCallback () {
super.connectedCallback();
this.initialize();
}
initialize () {
this.listenTo(this.model, 'change:role', this.onSearchRoleChange, this);
this.listenTo(this.model, 'change:affiliation', this.onSearchAffiliationChange, this);
}
render () {
const occupant = this.muc.occupants.findWhere({ 'jid': _converse.bare_jid });
return tpl_moderator_tools(
Object.assign(this.model.toJSON(), {
'affiliations_filter': this.affiliations_filter,
'alert_message': this.alert_message,
'alert_type': this.alert_type,
'assignAffiliation': ev => this.assignAffiliation(ev),
'assignRole': ev => this.assignRole(ev),
'assignable_affiliations': getAssignableAffiliations(occupant),
'assignable_roles': getAssignableRoles(occupant),
'filterAffiliationResults': ev => this.filterAffiliationResults(ev),
'filterRoleResults': ev => this.filterRoleResults(ev),
'loading_users_with_affiliation': this.loading_users_with_affiliation,
'queryAffiliation': ev => this.queryAffiliation(ev),
'queryRole': ev => this.queryRole(ev),
'queryable_affiliations': AFFILIATIONS.filter(a => !api.settings.get('modtools_disable_query').includes(a)),
'queryable_roles': ROLES.filter(a => !api.settings.get('modtools_disable_query').includes(a)),
'roles_filter': this.roles_filter,
'switchTab': ev => this.switchTab(ev),
'toggleForm': ev => this.toggleForm(ev),
'users_with_affiliation': this.users_with_affiliation,
'users_with_role': this.users_with_role,
})
);
}
async onSearchAffiliationChange () {
this.clearAlert();
this.loading_users_with_affiliation = true;
this.users_with_affiliation = null;
const affiliation = this.model.get('affiliation');
if (this.shouldFetchAffiliationsList()) {
const muc_jid = this.muc.get('jid');
const result = await getAffiliationList(affiliation, muc_jid);
if (result instanceof Error) {
this.alert(result.message, 'danger');
this.users_with_affiliation = [];
} else {
this.users_with_affiliation = result;
}
} else {
this.users_with_affiliation = this.muc.getOccupantsWithAffiliation(affiliation);
}
this.loading_users_with_affiliation = false;
}
onSearchRoleChange () {
this.clearAlert();
this.users_with_role = this.muc.getOccupantsWithRole(this.model.get('role'));
}
shouldFetchAffiliationsList () {
const affiliation = this.model.get('affiliation');
if (affiliation === 'none') {
return false;
}
const chatroom = this.muc;
const auto_fetched_affs = chatroom.occupants.getAutoFetchedAffiliationLists();
if (auto_fetched_affs.includes(affiliation)) {
return false;
} else {
return true;
}
}
toggleForm (ev) { // eslint-disable-line class-methods-use-this
ev.stopPropagation();
ev.preventDefault();
const form_class = ev.target.getAttribute('data-form');
const form = u.ancestor(ev.target, '.list-group-item').querySelector(`.${form_class}`);
if (u.hasClass('hidden', form)) {
u.removeClass('hidden', form);
} else {
u.addClass('hidden', form);
}
}
filterRoleResults (ev) {
this.roles_filter = ev.target.value;
this.render();
}
filterAffiliationResults (ev) {
this.affiliations_filter = ev.target.value;
}
queryRole (ev) {
ev.stopPropagation();
ev.preventDefault();
const data = new FormData(ev.target);
const role = data.get('role');
this.model.set({ 'role': null }, { 'silent': true });
this.model.set({ 'role': role });
}
queryAffiliation (ev) {
ev.stopPropagation();
ev.preventDefault();
const data = new FormData(ev.target);
const affiliation = data.get('affiliation');
this.model.set({ 'affiliation': null }, { 'silent': true });
this.model.set({ 'affiliation': affiliation });
}
alert (message, type) {
this.alert_message = message;
this.alert_type = type;
}
clearAlert () {
this.alert_message = undefined;
this.alert_type = undefined;
}
async assignAffiliation (ev) {
ev.stopPropagation();
ev.preventDefault();
this.clearAlert();
const data = new FormData(ev.target);
const affiliation = data.get('affiliation');
const attrs = {
'jid': data.get('jid'),
'reason': data.get('reason'),
};
const current_affiliation = this.model.get('affiliation');
const muc_jid = this.muc.get('jid');
try {
await setAffiliation(affiliation, muc_jid, [attrs]);
} catch (e) {
if (e === null) {
this.alert(__('Timeout error while trying to set the affiliation'), 'danger');
} else if (sizzle(`not-allowed[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
this.alert(__("Sorry, you're not allowed to make that change"), 'danger');
} else {
this.alert(__('Sorry, something went wrong while trying to set the affiliation'), 'danger');
}
log.error(e);
return;
}
await this.muc.occupants.fetchMembers();
this.model.set({ 'affiliation': null }, { 'silent': true });
this.model.set({ 'affiliation': current_affiliation });
this.alert(__('Affiliation changed'), 'primary');
}
assignRole (ev) {
ev.stopPropagation();
ev.preventDefault();
this.clearAlert();
const data = new FormData(ev.target);
const occupant = this.muc.getOccupant(data.get('jid') || data.get('nick'));
const role = data.get('role');
const reason = data.get('reason');
const current_role = this.model.get('role');
this.muc.setRole(
occupant,
role,
reason,
() => {
this.alert(__('Role changed'), 'primary');
this.model.set({ 'role': null }, { 'silent': true });
this.model.set({ 'role': current_role });
},
e => {
if (sizzle(`not-allowed[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
this.alert(__("You're not allowed to make that change"), 'danger');
} else {
this.alert(__('Sorry, something went wrong while trying to set the role'), 'danger');
if (u.isErrorObject(e)) {
log.error(e);
}
}
}
);
}
}
api.elements.define('converse-modtools', ModeratorTools);

View File

@ -1,7 +1,6 @@
import spinner from "templates/spinner.js";
import { __ } from 'i18n';
import { html } from "lit";
import { modal_header_close_button } from "modals/templates/buttons.js"
function getRoleHelpText (role) {
@ -135,13 +134,13 @@ const affiliation_list_item = (o) => html`
`;
const tpl_navigation = (o) => html`
const tpl_navigation = () => html`
<ul class="nav nav-pills justify-content-center">
<li role="presentation" class="nav-item">
<a class="nav-link active" id="affiliations-tab" href="#affiliations-tabpanel" aria-controls="affiliations-tabpanel" role="tab" data-toggle="tab" @click=${o.switchTab}>Affiliations</a>
<a class="nav-link active" id="affiliations-tab" href="#affiliations-tabpanel" aria-controls="affiliations-tabpanel" role="tab" data-toggle="tab">Affiliations</a>
</li>
<li role="presentation" class="nav-item">
<a class="nav-link" id="roles-tab" href="#roles-tabpanel" aria-controls="roles-tabpanel" role="tab" data-toggle="tab" @click=${o.switchTab}>Roles</a>
<a class="nav-link" id="roles-tab" href="#roles-tabpanel" aria-controls="roles-tabpanel" role="tab" data-toggle="tab">Roles</a>
</li>
</ul>
`;
@ -149,7 +148,6 @@ const tpl_navigation = (o) => html`
export default (o) => {
const i18n_affiliation = __('Affiliation');
const i18n_moderator_tools = __('Moderator Tools');
const i18n_no_users_with_aff = __('No users with that affiliation found.')
const i18n_no_users_with_role = __('No users with that role found.');
const i18n_filter = __('Type here to filter the search results');
@ -167,94 +165,83 @@ export default (o) => {
);
const show_both_tabs = o.queryable_roles.length && o.queryable_affiliations.length;
return html`
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="converse-modtools-modal-label">${i18n_moderator_tools}</h5>
${modal_header_close_button}
</div>
<div class="modal-body d-flex flex-column">
<span class="modal-alert"></span>
${o.alert_message ? html`<div class="alert alert-${o.alert_type}" role="alert">${o.alert_message}</div>` : '' }
${ show_both_tabs ? tpl_navigation() : '' }
${ show_both_tabs ? tpl_navigation(o) : '' }
<div class="tab-content">
<div class="tab-pane tab-pane--columns ${ o.queryable_affiliations.length ? 'active' : ''}" id="affiliations-tabpanel" role="tabpanel" aria-labelledby="affiliations-tab">
<form class="converse-form query-affiliation" @submit=${o.queryAffiliation}>
<p class="helptext pb-3">${i18n_helptext_affiliation}</p>
<div class="form-group">
<label for="affiliation">
<strong>${i18n_affiliation}:</strong>
</label>
<div class="row">
<div class="col">
<select class="custom-select select-affiliation" name="affiliation">
${o.queryable_affiliations.map(item => affiliation_option(Object.assign({item}, o)))}
</select>
</div>
<div class="col">
<input type="submit" class="btn btn-primary" name="users_with_affiliation" value="${i18n_show_users}"/>
</div>
</div>
<div class="row">
<div class="col mt-3">
${ (Array.isArray(o.users_with_affiliation) && o.users_with_affiliation.length > 5) ?
html`<input class="form-control" .value="${o.affiliations_filter}" @keyup=${o.filterAffiliationResults} type="text" name="filter" placeholder="${i18n_filter}"/>` : '' }
</div>
</div>
${ getAffiliationHelpText(o.affiliation) ?
html`<div class="row"><div class="col pt-2"><p class="helptext pb-3">${getAffiliationHelpText(o.affiliation)}</p></div></div>` : '' }
<div class="tab-content">
<div class="tab-pane tab-pane--columns ${ o.queryable_affiliations.length ? 'active' : ''}" id="affiliations-tabpanel" role="tabpanel" aria-labelledby="affiliations-tab">
<form class="converse-form query-affiliation" @submit=${o.queryAffiliation}>
<p class="helptext pb-3">${i18n_helptext_affiliation}</p>
<div class="form-group">
<label for="affiliation">
<strong>${i18n_affiliation}:</strong>
</label>
<div class="row">
<div class="col">
<select class="custom-select select-affiliation" name="affiliation">
${o.queryable_affiliations.map(item => affiliation_option(Object.assign({item}, o)))}
</select>
</div>
</form>
<div class="scrollable-container">
<ul class="list-group list-group--users">
${ (o.loading_users_with_affiliation) ? html`<li class="list-group-item"> ${spinner()} </li>` : '' }
${ (Array.isArray(o.users_with_affiliation) && o.users_with_affiliation.length === 0) ?
html`<li class="list-group-item">${i18n_no_users_with_aff}</li>` : '' }
${ (o.users_with_affiliation instanceof Error) ?
html`<li class="list-group-item">${o.users_with_affiliation.message}</li>` :
(o.users_with_affiliation || []).map(item => ((item.nick || item.jid).match(new RegExp(o.affiliations_filter, 'i')) ? affiliation_list_item(Object.assign({item}, o)) : '')) }
</ul>
</div>
</div>
<div class="tab-pane tab-pane--columns ${ !show_both_tabs && o.queryable_roles.length ? 'active' : ''}" id="roles-tabpanel" role="tabpanel" aria-labelledby="roles-tab">
<form class="converse-form query-role" @submit=${o.queryRole}>
<p class="helptext pb-3">${i18n_helptext_role}</p>
<div class="form-group">
<label for="role"><strong>${i18n_role}:</strong></label>
<div class="row">
<div class="col">
<select class="custom-select select-role" name="role">
${o.queryable_roles.map(item => role_option(Object.assign({item}, o)))}
</select>
</div>
<div class="col">
<input type="submit" class="btn btn-primary" name="users_with_role" value="${i18n_show_users}"/>
</div>
</div>
<div class="row">
<div class="col mt-3">
${ (Array.isArray(o.users_with_role) && o.users_with_role.length > 5) ?
html`<input class="form-control" .value="${o.roles_filter}" @keyup=${o.filterRoleResults} type="text" name="filter" placeholder="${i18n_filter}"/>` : '' }
</div>
</div>
${ getRoleHelpText(o.role) ? html`<div class="row"><div class="col pt-2"><p class="helptext pb-3">${getRoleHelpText(o.role)}</p></div></div>` : ''}
<div class="col">
<input type="submit" class="btn btn-primary" name="users_with_affiliation" value="${i18n_show_users}"/>
</div>
</form>
<div class="scrollable-container">
<ul class="list-group list-group--users">
${ o.loading_users_with_role ? html`<li class="list-group-item"> ${spinner()} </li>` : '' }
${ (o.users_with_role && o.users_with_role.length === 0) ? html`<li class="list-group-item">${i18n_no_users_with_role}</li>` : '' }
${ (o.users_with_role || []).map(item => (item.nick.match(o.roles_filter) ? role_list_item(Object.assign({item}, o)) : '')) }
</ul>
</div>
<div class="row">
<div class="col mt-3">
${ (Array.isArray(o.users_with_affiliation) && o.users_with_affiliation.length > 5) ?
html`<input class="form-control" .value="${o.affiliations_filter}" @keyup=${o.filterAffiliationResults} type="text" name="filter" placeholder="${i18n_filter}"/>` : '' }
</div>
</div>
${ getAffiliationHelpText(o.affiliation) ?
html`<div class="row"><div class="col pt-2"><p class="helptext pb-3">${getAffiliationHelpText(o.affiliation)}</p></div></div>` : '' }
</div>
</form>
<div class="scrollable-container">
<ul class="list-group list-group--users">
${ (o.loading_users_with_affiliation) ? html`<li class="list-group-item"> ${spinner()} </li>` : '' }
${ (Array.isArray(o.users_with_affiliation) && o.users_with_affiliation.length === 0) ?
html`<li class="list-group-item">${i18n_no_users_with_aff}</li>` : '' }
${ (o.users_with_affiliation instanceof Error) ?
html`<li class="list-group-item">${o.users_with_affiliation.message}</li>` :
(o.users_with_affiliation || []).map(item => ((item.nick || item.jid).match(new RegExp(o.affiliations_filter, 'i')) ? affiliation_list_item(Object.assign({item}, o)) : '')) }
</ul>
</div>
</div>
</div>
</div>`;
<div class="tab-pane tab-pane--columns ${ !show_both_tabs && o.queryable_roles.length ? 'active' : ''}" id="roles-tabpanel" role="tabpanel" aria-labelledby="roles-tab">
<form class="converse-form query-role" @submit=${o.queryRole}>
<p class="helptext pb-3">${i18n_helptext_role}</p>
<div class="form-group">
<label for="role"><strong>${i18n_role}:</strong></label>
<div class="row">
<div class="col">
<select class="custom-select select-role" name="role">
${o.queryable_roles.map(item => role_option(Object.assign({item}, o)))}
</select>
</div>
<div class="col">
<input type="submit" class="btn btn-primary" name="users_with_role" value="${i18n_show_users}"/>
</div>
</div>
<div class="row">
<div class="col mt-3">
${ (Array.isArray(o.users_with_role) && o.users_with_role.length > 5) ?
html`<input class="form-control" .value="${o.roles_filter}" @keyup=${o.filterRoleResults} type="text" name="filter" placeholder="${i18n_filter}"/>` : '' }
</div>
</div>
${ getRoleHelpText(o.role) ? html`<div class="row"><div class="col pt-2"><p class="helptext pb-3">${getRoleHelpText(o.role)}</p></div></div>` : ''}
</div>
</form>
<div class="scrollable-container">
<ul class="list-group list-group--users">
${ o.loading_users_with_role ? html`<li class="list-group-item"> ${spinner()} </li>` : '' }
${ (o.users_with_role && o.users_with_role.length === 0) ? html`<li class="list-group-item">${i18n_no_users_with_role}</li>` : '' }
${ (o.users_with_role || []).map(item => (item.nick.match(o.roles_filter) ? role_list_item(Object.assign({item}, o)) : '')) }
</ul>
</div>
</div>
</div>`;
}

View File

@ -338,9 +338,12 @@ describe("The groupchat moderator tool", function () {
_converse.connection._dataRecv(mock.createRequest(error));
await u.waitUntil(() => !modal.loading_users_with_affiliation);
const alert = await u.waitUntil(() => modal.el.querySelector('.alert'));
expect(alert.textContent.trim()).toBe('Error: not allowed to fetch outcast list for MUC lounge@montague.lit');
const user_els = modal.el.querySelectorAll('.list-group--users > li');
expect(user_els.length).toBe(1);
expect(user_els[0].textContent.trim()).toBe('Error: not allowed to fetch outcast list for MUC lounge@montague.lit');
expect(user_els[0].textContent.trim()).toBe('No users with that affiliation found.');
}));
it("shows an error message if a particular affiliation may not be set",
@ -452,23 +455,23 @@ describe("The groupchat moderator tool", function () {
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(enter);
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid});
expect(modal.getAssignableAffiliations(occupant)).toEqual(['owner', 'admin', 'member', 'outcast', 'none']);
expect(_converse.getAssignableAffiliations(occupant)).toEqual(['owner', 'admin', 'member', 'outcast', 'none']);
_converse.api.settings.set('modtools_disable_assign', ['owner']);
expect(modal.getAssignableAffiliations(occupant)).toEqual(['admin', 'member', 'outcast', 'none']);
expect(_converse.getAssignableAffiliations(occupant)).toEqual(['admin', 'member', 'outcast', 'none']);
_converse.api.settings.set('modtools_disable_assign', ['owner', 'admin']);
expect(modal.getAssignableAffiliations(occupant)).toEqual(['member', 'outcast', 'none']);
expect(_converse.getAssignableAffiliations(occupant)).toEqual(['member', 'outcast', 'none']);
_converse.api.settings.set('modtools_disable_assign', ['owner', 'admin', 'outcast']);
expect(modal.getAssignableAffiliations(occupant)).toEqual(['member', 'none']);
expect(_converse.getAssignableAffiliations(occupant)).toEqual(['member', 'none']);
expect(modal.getAssignableRoles(occupant)).toEqual(['moderator', 'participant', 'visitor']);
expect(_converse.getAssignableRoles(occupant)).toEqual(['moderator', 'participant', 'visitor']);
_converse.api.settings.set('modtools_disable_assign', ['admin', 'moderator']);
expect(modal.getAssignableRoles(occupant)).toEqual(['participant', 'visitor']);
expect(_converse.getAssignableRoles(occupant)).toEqual(['participant', 'visitor']);
}));
});

View File

@ -27,8 +27,8 @@
message_limit: 300,
auto_register_muc_nickname: true,
loglevel: 'debug',
modtools_disable_assign: ['owner', 'moderator', 'participant', 'visitor'],
modtools_disable_query: ['moderator', 'participant', 'visitor'],
// modtools_disable_assign: ['owner', 'moderator', 'participant', 'visitor'],
// modtools_disable_query: ['moderator', 'participant', 'visitor'],
enable_smacks: true,
// connection_options: { 'worker': '/dist/shared-connection-worker.js' },
// persistent_store: 'IndexedDB',