Turn the MUC affiliation form into a component

So that it can be used elsewhere, for example in the occupant modal.
This commit is contained in:
JC Brand 2023-02-21 10:48:05 +01:00
parent 6ce8879e9c
commit 57f489f61b
7 changed files with 147 additions and 95 deletions

View File

@ -45,7 +45,7 @@ export async function getAffiliationList (affiliation, muc_jid) {
}
/**
* Given an occupant model, see which affiliations may be assigned to that user.
* Given an occupant model, see which affiliations may be assigned by that user
* @param { Model } occupant
* @returns { Array<('owner'|'admin'|'member'|'outcast'|'none')> } - An array of assignable affiliations
*/
@ -54,9 +54,9 @@ export function getAssignableAffiliations (occupant) {
if (!Array.isArray(disabled)) {
disabled = disabled ? AFFILIATIONS : [];
}
if (occupant.get('affiliation') === 'owner') {
if (occupant?.get('affiliation') === 'owner') {
return AFFILIATIONS.filter(a => !disabled.includes(a));
} else if (occupant.get('affiliation') === 'admin') {
} else if (occupant?.get('affiliation') === 'admin') {
return AFFILIATIONS.filter(a => !['owner', 'admin', ...disabled].includes(a));
} else {
return [];

View File

@ -0,0 +1,69 @@
import log from '@converse/headless/log';
import tplAffiliationForm from './templates/affiliation-form.js';
import { CustomElement } from 'shared/components/element';
import { __ } from 'i18n';
import { api, converse } from '@converse/headless/core';
import { setAffiliation } from '@converse/headless/plugins/muc/affiliations/utils.js';
const { Strophe, sizzle } = converse.env;
class AffiliationForm extends CustomElement {
static get properties () {
return {
muc: { type: Object },
jid: { type: String },
affiliation: { type: String },
alert_message: { type: String, attribute: false },
alert_type: { type: String, attribute: false },
};
}
render () {
return tplAffiliationForm(this);
}
alert (message, type) {
this.alert_message = message;
this.alert_type = type;
}
async assignAffiliation (ev) {
ev.stopPropagation();
ev.preventDefault();
this.alert(); // clear alert messages
const data = new FormData(ev.target);
const affiliation = data.get('affiliation');
const attrs = {
jid: this.jid,
reason: data.get('reason'),
};
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();
/**
* @event affiliationChanged
* @example
* const el = document.querySelector('converse-muc-affiliation-form');
* el.addEventListener('affiliationChanged', () => { ... });
*/
const event = new CustomEvent('affiliationChanged', { bubbles: true });
this.dispatchEvent(event);
}
}
api.elements.define('converse-muc-affiliation-form', AffiliationForm);

View File

@ -4,6 +4,7 @@
* @license Mozilla Public License (MPLv2)
*/
import '../chatboxviews/index.js';
import './affiliation-form.js';
import MUCView from './muc.js';
import { api, converse } from '@converse/headless/core.js';
import { clearHistory, parseMessageForMUCCommands } from './utils.js';

View File

@ -3,14 +3,10 @@ import tplModeratorTools from './templates/moderator-tools.js';
import { AFFILIATIONS, ROLES } from '@converse/headless/plugins/muc/constants.js';
import { CustomElement } from 'shared/components/element.js';
import { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core.js';
import { api, converse } from '@converse/headless/core.js';
import { getAffiliationList, getAssignableAffiliations } from '@converse/headless/plugins/muc/affiliations/utils.js';
import { getAssignableRoles, getAutoFetchedAffiliationLists } from '@converse/headless/plugins/muc/utils.js';
import { getOpenPromise } from '@converse/openpromise';
import {
getAffiliationList,
getAssignableAffiliations,
setAffiliation,
} from '@converse/headless/plugins/muc/affiliations/utils.js';
import './styles/moderator-tools.scss';
@ -40,6 +36,12 @@ export default class ModeratorTools extends CustomElement {
this.affiliations_filter = '';
this.role = '';
this.roles_filter = '';
this.addEventListener("affiliationChanged", () => {
this.alert(__('Affiliation changed'), 'primary');
this.onSearchAffiliationChange();
this.requestUpdate()
});
}
updated (changed) {
@ -58,12 +60,11 @@ export default class ModeratorTools extends CustomElement {
render () {
if (this.muc?.occupants) {
const occupant = this.muc.occupants.findWhere({ 'jid': _converse.bare_jid });
return tplModeratorTools({
const occupant = this.muc.occupants.getOwnOccupant();
return tplModeratorTools(this, {
'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),
@ -72,7 +73,9 @@ export default class ModeratorTools extends CustomElement {
'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_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),
@ -80,7 +83,7 @@ export default class ModeratorTools extends CustomElement {
'toggleForm': ev => this.toggleForm(ev),
'users_with_affiliation': this.users_with_affiliation,
'users_with_role': this.users_with_role,
})
});
} else {
return '';
}
@ -94,9 +97,8 @@ export default class ModeratorTools extends CustomElement {
}
async onSearchAffiliationChange () {
if (!this.affiliation) {
return;
}
if (!this.affiliation) return;
await this.initialized;
this.clearAlert();
this.loading_users_with_affiliation = true;
@ -138,12 +140,13 @@ export default class ModeratorTools extends CustomElement {
}
}
toggleForm (ev) { // eslint-disable-line class-methods-use-this
// eslint-disable-next-line class-methods-use-this
toggleForm (ev) {
ev.stopPropagation();
ev.preventDefault();
const toggle = u.ancestor(ev.target, '.toggle-form');
const form_class = toggle.getAttribute('data-form');
const form = u.ancestor(toggle, '.list-group-item').querySelector(`.${form_class}`);
const sel = toggle.getAttribute('data-form');
const form = u.ancestor(toggle, '.list-group-item').querySelector(sel);
if (u.hasClass('hidden', form)) {
u.removeClass('hidden', form);
} else {
@ -188,37 +191,6 @@ export default class ModeratorTools extends CustomElement {
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.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.affiliation = null;
this.affiliation = current_affiliation;
this.alert(__('Affiliation changed'), 'primary');
}
assignRole (ev) {
ev.stopPropagation();
ev.preventDefault();

View File

@ -0,0 +1,36 @@
import { __ } from 'i18n';
import { html } from "lit";
import { getAssignableAffiliations } from '@converse/headless/plugins/muc/affiliations/utils.js';
export default (el) => {
const i18n_change_affiliation = __('Change affiliation');
const i18n_new_affiliation = __('New affiliation');
const i18n_reason = __('Reason');
const occupant = el.muc.getOwnOccupant();
const assignable_affiliations = getAssignableAffiliations(occupant);
return html`
<form class="affiliation-form" @submit=${ev => el.assignAffiliation(ev)}>
${el.alert_message ? html`<div class="alert alert-${el.alert_type}" role="alert">${el.alert_message}</div>` : '' }
<div class="form-group">
<div class="row">
<div class="col">
<label><strong>${i18n_new_affiliation}:</strong></label>
<select class="custom-select select-affiliation" name="affiliation">
${ assignable_affiliations.map(aff => html`<option value="${aff}" ?selected=${aff === el.affiliation}>${aff}</option>`) }
</select>
</div>
<div class="col">
<label><strong>${i18n_reason}:</strong></label>
<input class="form-control" type="text" name="reason"/>
</div>
</div>
</div>
<div class="form-group">
<div class="col">
<input type="submit" class="btn btn-primary" name="change" value="${i18n_change_affiliation}"/>
</div>
</div>
</form>
`;
}

View File

@ -94,45 +94,13 @@ const role_list_item = (o) => html`
`;
const tplSetAffiliationForm = (o) => {
const i18n_change_affiliation = __('Change affiliation');
const i18n_new_affiliation = __('New affiliation');
const i18n_reason = __('Reason');
return html`
<form class="affiliation-form hidden" @submit=${o.assignAffiliation}>
<div class="form-group">
<input type="hidden" name="jid" value="${o.item.jid}"/>
<input type="hidden" name="nick" value="${o.item.nick}"/>
<div class="row">
<div class="col">
<label><strong>${i18n_new_affiliation}:</strong></label>
<select class="custom-select select-affiliation" name="affiliation">
${ o.assignable_affiliations.map(aff => html`<option value="${aff}" ?selected=${aff === o.item.affiliation}>${aff}</option>`) }
</select>
</div>
<div class="col">
<label><strong>${i18n_reason}:</strong></label>
<input class="form-control" type="text" name="reason"/>
</div>
</div>
</div>
<div class="form-group">
<div class="col">
<input type="submit" class="btn btn-primary" name="change" value="${i18n_change_affiliation}"/>
</div>
</div>
</form>
`;
}
const affiliation_form_toggle = (o) => html`
<a href="#" data-form="affiliation-form" class="toggle-form right" color="var(--subdued-color)" @click=${o.toggleForm}>
<a href="#" data-form="converse-muc-affiliation-form" class="toggle-form right" color="var(--subdued-color)" @click=${o.toggleForm}>
<converse-icon class="fa fa-wrench" size="1em"></converse-icon>
</a>`;
const affiliation_list_item = (o) => html`
const affiliation_list_item = (el, o) => html`
<li class="list-group-item" data-nick="${o.item.nick}">
<ul class="list-group">
<li class="list-group-item active">
@ -143,7 +111,9 @@ const affiliation_list_item = (o) => html`
</li>
<li class="list-group-item">
<div><strong>Affiliation:</strong> ${o.item.affiliation} ${o.assignable_affiliations.length ? affiliation_form_toggle(o) : ''}</div>
${o.assignable_affiliations.length ? tplSetAffiliationForm(o) : ''}
${o.assignable_affiliations.length ?
html`<converse-muc-affiliation-form class="hidden" .muc=${el.muc} jid=${o.item.jid} affiliation=${o.item.affiliation}></converse-muc-affiliation-form>` : ''
}
</li>
</ul>
</li>
@ -174,7 +144,7 @@ const tplNavigation = (o) => html`
`;
export default (o) => {
export default (el, o) => {
const i18n_affiliation = __('Affiliation');
const i18n_no_users_with_aff = __('No users with that affiliation found.')
const i18n_no_users_with_role = __('No users with that role found.');
@ -235,7 +205,7 @@ export default (o) => {
${ (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)) : '')) }
(o.users_with_affiliation || []).map(item => ((item.nick || item.jid).match(new RegExp(o.affiliations_filter, 'i')) ? affiliation_list_item(el, Object.assign({item}, o)) : '')) }
</ul>
</div>
</div>` : '' }

View File

@ -37,7 +37,7 @@ describe("The groupchat moderator tool", function () {
await u.waitUntil(() => (view.model.occupants.length === 5), 1000);
const modal = await openModtools(_converse, view);
let tab = modal.querySelector('#affiliations-tab');
const tab = modal.querySelector('#affiliations-tab');
// Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = [];
tab.click();
@ -70,10 +70,12 @@ describe("The groupchat moderator tool", function () {
expect(user_els[1].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner');
const toggle = user_els[1].querySelector('.list-group-item:nth-child(3n) .toggle-form');
const form = user_els[1].querySelector('.list-group-item:nth-child(3n) .affiliation-form');
expect(u.hasClass('hidden', form)).toBeTruthy();
const component = user_els[1].querySelector('.list-group-item:nth-child(3n) converse-muc-affiliation-form');
expect(u.hasClass('hidden', component)).toBeTruthy();
toggle.click();
expect(u.hasClass('hidden', form)).toBeFalsy();
expect(u.hasClass('hidden', component)).toBeFalsy();
const form = user_els[1].querySelector('.list-group-item:nth-child(3n) .affiliation-form');
select = form.querySelector('.select-affiliation');
expect(select.value).toBe('owner');
select.value = 'admin';
@ -372,10 +374,12 @@ describe("The groupchat moderator tool", function () {
const user_els = modal.querySelectorAll('.list-group--users > li');
const toggle = user_els[0].querySelector('.list-group-item:nth-child(3n) .toggle-form');
const form = user_els[0].querySelector('.list-group-item:nth-child(3n) .affiliation-form');
expect(u.hasClass('hidden', form)).toBeTruthy();
const component = user_els[0].querySelector('.list-group-item:nth-child(3n) converse-muc-affiliation-form');
expect(u.hasClass('hidden', component)).toBeTruthy();
toggle.click();
expect(u.hasClass('hidden', form)).toBeFalsy();
expect(u.hasClass('hidden', component)).toBeFalsy();
const form = user_els[0].querySelector('.list-group-item:nth-child(3n) .affiliation-form');
const change_affiliation_dropdown = form.querySelector('.select-affiliation');
expect(change_affiliation_dropdown.value).toBe('member');
change_affiliation_dropdown.value = 'admin';