Add-hoc form fixes
- Provide actions as received in the Ad-Hoc form - Add support for multi-stage ad-hoc forms - Add new tests for multi-stage forms Fixes #2240
This commit is contained in:
parent
5029d93523
commit
0fcdb2a594
@ -2,6 +2,7 @@
|
||||
|
||||
## 10.1.1 (Unreleased)
|
||||
|
||||
- #2240: Ad-Hoc command result form not shown
|
||||
- #3128: Second bookmarked room shows info of the first one
|
||||
- Bugfix. Uyghur translations weren't loading
|
||||
|
||||
|
@ -453,13 +453,16 @@ export const api = _converse.api = {
|
||||
* nothing to wait for, so an already resolved promise is returned.
|
||||
*/
|
||||
sendIQ (stanza, timeout=_converse.STANZA_TIMEOUT, reject=true) {
|
||||
|
||||
const { connection } = _converse;
|
||||
|
||||
let promise;
|
||||
stanza = stanza.tree?.() ?? stanza;
|
||||
if (['get', 'set'].includes(stanza.getAttribute('type'))) {
|
||||
timeout = timeout || _converse.STANZA_TIMEOUT;
|
||||
if (reject) {
|
||||
promise = new Promise((resolve, reject) => _converse.connection.sendIQ(stanza, resolve, reject, timeout));
|
||||
promise.catch(e => {
|
||||
promise = new Promise((resolve, reject) => connection.sendIQ(stanza, resolve, reject, timeout));
|
||||
promise.catch((e) => {
|
||||
if (e === null) {
|
||||
throw new TimeoutError(
|
||||
`Timeout error after ${timeout}ms for the following IQ stanza: ${Strophe.serialize(stanza)}`
|
||||
@ -467,7 +470,7 @@ export const api = _converse.api = {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
promise = new Promise(resolve => _converse.connection.sendIQ(stanza, resolve, resolve, timeout));
|
||||
promise = new Promise((resolve) => connection.sendIQ(stanza, resolve, resolve, timeout));
|
||||
}
|
||||
} else {
|
||||
_converse.connection.sendIQ(stanza);
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { converse } from "../core.js";
|
||||
import log from "@converse/headless/log";
|
||||
import sizzle from 'sizzle';
|
||||
import { __ } from 'i18n';
|
||||
import { converse } from "../core.js";
|
||||
import { getAttributes } from '@converse/headless/shared/parsers';
|
||||
|
||||
const { Strophe } = converse.env;
|
||||
let _converse, api;
|
||||
const { Strophe, u, stx, $iq } = converse.env;
|
||||
let api;
|
||||
|
||||
Strophe.addNamespace('ADHOC', 'http://jabber.org/protocol/commands');
|
||||
|
||||
@ -14,6 +15,18 @@ function parseForCommands (stanza) {
|
||||
return items.map(getAttributes)
|
||||
}
|
||||
|
||||
function getCommandFields (iq, jid) {
|
||||
const cmd_el = sizzle(`command[xmlns="${Strophe.NS.ADHOC}"]`, iq).pop();
|
||||
const data = {
|
||||
sessionid: cmd_el.getAttribute('sessionid'),
|
||||
instructions: sizzle('x[type="form"][xmlns="jabber:x:data"] instructions', cmd_el).pop()?.textContent,
|
||||
fields: sizzle('x[type="form"][xmlns="jabber:x:data"] field', cmd_el)
|
||||
.map(f => u.xForm2TemplateResult(f, cmd_el, { domain: jid })),
|
||||
actions: Array.from(cmd_el.querySelector('actions')?.children).map((a) => a.nodeName.toLowerCase()) ?? []
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
const adhoc_api = {
|
||||
/**
|
||||
@ -30,9 +43,8 @@ const adhoc_api = {
|
||||
* @param { String } to_jid
|
||||
*/
|
||||
async getCommands (to_jid) {
|
||||
let commands = [];
|
||||
try {
|
||||
commands = parseForCommands(await api.disco.items(to_jid, Strophe.NS.ADHOC));
|
||||
return parseForCommands(await api.disco.items(to_jid, Strophe.NS.ADHOC));
|
||||
} catch (e) {
|
||||
if (e === null) {
|
||||
log.error(`Error: timeout while fetching ad-hoc commands for ${to_jid}`);
|
||||
@ -40,8 +52,78 @@ const adhoc_api = {
|
||||
log.error(`Error while fetching ad-hoc commands for ${to_jid}`);
|
||||
log.error(e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @method api.adhoc.fetchCommandForm
|
||||
*/
|
||||
async fetchCommandForm (command) {
|
||||
const node = command.node;
|
||||
const jid = command.jid;
|
||||
const stanza = $iq({
|
||||
'type': 'set',
|
||||
'to': jid
|
||||
}).c('command', {
|
||||
'xmlns': Strophe.NS.ADHOC,
|
||||
'node': node,
|
||||
'action': 'execute'
|
||||
});
|
||||
try {
|
||||
return getCommandFields(await api.sendIQ(stanza), jid);
|
||||
|
||||
} catch (e) {
|
||||
if (e === null) {
|
||||
log.error(`Error: timeout while trying to execute command for ${jid}`);
|
||||
} else {
|
||||
log.error(`Error while trying to execute command for ${jid}`);
|
||||
log.error(e);
|
||||
}
|
||||
return {
|
||||
instructions: __('An error occurred while trying to fetch the command form'),
|
||||
fields: []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @method api.adhoc.runCommand
|
||||
* @param { String } jid
|
||||
* @param { String } sessionid
|
||||
* @param { 'execute' | 'cancel' | 'prev' | 'next' | 'complete' } action
|
||||
* @param { String } node
|
||||
* @param { Array<{ string: string }> } inputs
|
||||
*/
|
||||
async runCommand (jid, sessionid, node, action, inputs) {
|
||||
const iq =
|
||||
stx`<iq type="set" to="${jid}" xmlns="jabber:client">
|
||||
<command sessionid="${sessionid}" node="${node}" action="${action}" xmlns="${Strophe.NS.ADHOC}">
|
||||
<x xmlns="${Strophe.NS.XFORM}" type="submit">
|
||||
${ inputs.reduce((out, { name, value }) => out + `<field var="${name}"><value>${value}</value></field>`, '') }
|
||||
</x>
|
||||
</command>
|
||||
</iq>`;
|
||||
|
||||
const result = await api.sendIQ(iq, null, false);
|
||||
if (result === null) {
|
||||
log.warn(`A timeout occurred while trying to run an ad-hoc command`);
|
||||
return {
|
||||
status: 'error',
|
||||
note: __('A timeout occurred'),
|
||||
}
|
||||
} else if (u.isErrorStanza(result)) {
|
||||
log.error('Error while trying to execute an ad-hoc command');
|
||||
log.error(result);
|
||||
}
|
||||
|
||||
const command = result.querySelector('command');
|
||||
const status = command?.getAttribute('status');
|
||||
return {
|
||||
status,
|
||||
...(status === 'executing' ? getCommandFields(result) : {}),
|
||||
note: result.querySelector('note')?.textContent
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,7 +134,7 @@ converse.plugins.add('converse-adhoc', {
|
||||
dependencies: ["converse-disco"],
|
||||
|
||||
initialize () {
|
||||
_converse = this._converse;
|
||||
const _converse = this._converse;
|
||||
api = _converse.api;
|
||||
Object.assign(api, adhoc_api);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import log from '../../log';
|
||||
import u from '../../utils/form';
|
||||
import { Strophe } from 'strophe.js/src/strophe';
|
||||
import { _converse, api } from '../../core.js';
|
||||
import { _converse, api, converse } from '../../core.js';
|
||||
|
||||
const { u } = converse.env;
|
||||
|
||||
|
||||
export default {
|
||||
|
@ -5,7 +5,6 @@ import log from '../../log';
|
||||
import p from '../../utils/parse-helpers';
|
||||
import pick from 'lodash-es/pick';
|
||||
import sizzle from 'sizzle';
|
||||
import u from '../../utils/form';
|
||||
import { Model } from '@converse/skeletor/src/model.js';
|
||||
import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe';
|
||||
import { _converse, api, converse } from '../../core.js';
|
||||
@ -19,6 +18,8 @@ import { parseMUCMessage, parseMUCPresence } from './parsers.js';
|
||||
import { sendMarker } from '../../shared/actions.js';
|
||||
import { ROOMSTATUS } from './constants.js';
|
||||
|
||||
const { u } = converse.env;
|
||||
|
||||
const OWNER_COMMANDS = ['owner'];
|
||||
const ADMIN_COMMANDS = ['admin', 'ban', 'deop', 'destroy', 'member', 'op', 'revoke'];
|
||||
const MODERATOR_COMMANDS = ['kick', 'mute', 'voice', 'modtools'];
|
||||
|
@ -1,14 +1,15 @@
|
||||
import ChatRoomOccupant from './occupant.js';
|
||||
import u from '../../utils/form';
|
||||
import { Collection } from '@converse/skeletor/src/collection.js';
|
||||
import { MUC_ROLE_WEIGHTS } from './constants.js';
|
||||
import { Model } from '@converse/skeletor/src/model.js';
|
||||
import { Strophe } from 'strophe.js/src/strophe.js';
|
||||
import { _converse, api } from '../../core.js';
|
||||
import { _converse, api, converse } from '../../core.js';
|
||||
import { getAffiliationList } from './affiliations/utils.js';
|
||||
import { getAutoFetchedAffiliationLists } from './utils.js';
|
||||
import { getUniqueId } from '@converse/headless/utils/core.js';
|
||||
|
||||
const { u } = converse.env;
|
||||
|
||||
|
||||
/**
|
||||
* A list of {@link _converse.ChatRoomOccupant} instances, representing participants in a MUC.
|
||||
|
@ -6,7 +6,6 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import _converse from '@converse/headless/shared/_converse.js';
|
||||
import compact from "lodash-es/compact";
|
||||
import isElement from "lodash-es/isElement";
|
||||
import isObject from "lodash-es/isObject";
|
||||
import last from "lodash-es/last";
|
||||
import log from '@converse/headless/log.js';
|
||||
@ -17,6 +16,10 @@ import { getOpenPromise } from '@converse/openpromise';
|
||||
import { settings_api } from '@converse/headless/shared/settings/api.js';
|
||||
import { stx , toStanza } from './stanza.js';
|
||||
|
||||
export function isElement (el) {
|
||||
return el instanceof Element || el instanceof HTMLDocument;
|
||||
}
|
||||
|
||||
export function isError (obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object Error]";
|
||||
}
|
||||
@ -619,6 +622,7 @@ export function saveWindowState (ev) {
|
||||
export default Object.assign({
|
||||
getRandomInt,
|
||||
getUniqueId,
|
||||
isElement,
|
||||
isEmptyMessage,
|
||||
isValidJID,
|
||||
merge,
|
||||
|
@ -5,13 +5,17 @@
|
||||
*/
|
||||
import u from "./core";
|
||||
|
||||
const tpl_xform_field = (name, value) => `<field var="${name}">${ value }</field>`;
|
||||
|
||||
const tpl_xform_value = (value) => `<value>${value}</value>`;
|
||||
|
||||
/**
|
||||
* Takes an HTML DOM and turns it into an XForm field.
|
||||
* @private
|
||||
* @method u#webForm2xForm
|
||||
* @param { DOMElement } field - the field to convert
|
||||
*/
|
||||
u.webForm2xForm = function (field) {
|
||||
export function webForm2xForm (field) {
|
||||
const name = field.getAttribute('name');
|
||||
if (!name) {
|
||||
return null; // See #1924
|
||||
@ -26,11 +30,10 @@ u.webForm2xForm = function (field) {
|
||||
} else {
|
||||
value = field.value;
|
||||
}
|
||||
return u.toStanza(`
|
||||
<field var="${name}">
|
||||
${ value.constructor === Array ?
|
||||
value.map(v => `<value>${v}</value>`) :
|
||||
`<value>${value}</value>` }
|
||||
</field>`);
|
||||
};
|
||||
export default u;
|
||||
return u.toStanza(tpl_xform_field(
|
||||
name,
|
||||
Array.isArray(value) ? value.map(tpl_xform_value) : tpl_xform_value(value),
|
||||
));
|
||||
}
|
||||
|
||||
u.webForm2xForm = webForm2xForm;
|
||||
|
@ -1,25 +1,24 @@
|
||||
import 'shared/autocomplete/index.js';
|
||||
import log from "@converse/headless/log";
|
||||
import log from '@converse/headless/log';
|
||||
import tpl_adhoc from './templates/ad-hoc.js';
|
||||
import { CustomElement } from 'shared/components/element.js';
|
||||
import { __ } from 'i18n';
|
||||
import { api, converse } from "@converse/headless/core";
|
||||
import { fetchCommandForm } from './utils.js';
|
||||
import { api, converse } from '@converse/headless/core.js';
|
||||
import { getNameAndValue } from 'utils/html.js';
|
||||
|
||||
const { Strophe, $iq, sizzle, u } = converse.env;
|
||||
const { Strophe, sizzle } = converse.env;
|
||||
|
||||
|
||||
export default class AdHocCommands extends CustomElement {
|
||||
|
||||
static get properties () {
|
||||
return {
|
||||
'alert': { type: String },
|
||||
'alert_type': { type: String },
|
||||
'nonce': { type: String }, // Used to force re-rendering
|
||||
'fetching': { type: Boolean }, // Used to force re-rendering
|
||||
'commands': { type: Array },
|
||||
'fetching': { type: Boolean },
|
||||
'showform': { type: String },
|
||||
'view': { type: String },
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor () {
|
||||
@ -31,12 +30,7 @@ export default class AdHocCommands extends CustomElement {
|
||||
}
|
||||
|
||||
render () {
|
||||
return tpl_adhoc(this, {
|
||||
'hideCommandForm': ev => this.hideCommandForm(ev),
|
||||
'runCommand': ev => this.runCommand(ev),
|
||||
'showform': this.showform,
|
||||
'toggleCommandForm': ev => this.toggleCommandForm(ev),
|
||||
});
|
||||
return tpl_adhoc(this)
|
||||
}
|
||||
|
||||
async fetchCommands (ev) {
|
||||
@ -50,7 +44,7 @@ export default class AdHocCommands extends CustomElement {
|
||||
const jid = form_data.get('jid').trim();
|
||||
let supported;
|
||||
try {
|
||||
supported = await api.disco.supports(Strophe.NS.ADHOC, jid)
|
||||
supported = await api.disco.supports(Strophe.NS.ADHOC, jid);
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
} finally {
|
||||
@ -79,59 +73,117 @@ export default class AdHocCommands extends CustomElement {
|
||||
ev.preventDefault();
|
||||
const node = ev.target.getAttribute('data-command-node');
|
||||
const cmd = this.commands.filter(c => c.node === node)[0];
|
||||
this.showform !== node && await fetchCommandForm(cmd);
|
||||
this.showform = node;
|
||||
if (this.showform === node) {
|
||||
this.showform = '';
|
||||
this.requestUpdate();
|
||||
} else {
|
||||
const form = await api.adhoc.fetchCommandForm(cmd);
|
||||
cmd.sessionid = form.sessionid;
|
||||
cmd.instructions = form.instructions;
|
||||
cmd.fields = form.fields;
|
||||
cmd.actions = form.actions;
|
||||
this.showform = node;
|
||||
}
|
||||
}
|
||||
|
||||
hideCommandForm (ev) {
|
||||
executeAction (ev) {
|
||||
ev.preventDefault();
|
||||
this.nonce = u.getUniqueId();
|
||||
this.showform = ''
|
||||
|
||||
const action = ev.target.getAttribute('data-action');
|
||||
|
||||
if (['execute', 'next', 'prev', 'complete'].includes(action)) {
|
||||
this.runCommand(ev.target.form, action);
|
||||
} else {
|
||||
log.error(`Unknown action: ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
async runCommand (ev) {
|
||||
ev.preventDefault();
|
||||
const form_data = new FormData(ev.target);
|
||||
clearCommand (cmd) {
|
||||
delete cmd.alert;
|
||||
delete cmd.instructions;
|
||||
delete cmd.sessionid;
|
||||
delete cmd.alert_type;
|
||||
cmd.fields = [];
|
||||
cmd.acions = [];
|
||||
this.showform = '';
|
||||
}
|
||||
|
||||
async runCommand (form, action) {
|
||||
const form_data = new FormData(form);
|
||||
const jid = form_data.get('command_jid').trim();
|
||||
const node = form_data.get('command_node').trim();
|
||||
|
||||
const cmd = this.commands.filter(c => c.node === node)[0];
|
||||
cmd.alert = null;
|
||||
this.nonce = u.getUniqueId();
|
||||
delete cmd.alert;
|
||||
this.requestUpdate();
|
||||
|
||||
const inputs = sizzle(':input:not([type=button]):not([type=submit])', ev.target);
|
||||
const config_array = inputs
|
||||
.filter(i => !['command_jid', 'command_node'].includes(i.getAttribute('name')))
|
||||
.map(u.webForm2xForm)
|
||||
.filter(n => n);
|
||||
const inputs = action === 'prev' ? [] :
|
||||
sizzle(':input:not([type=button]):not([type=submit])', form)
|
||||
.filter(i => !['command_jid', 'command_node'].includes(i.getAttribute('name')))
|
||||
.map(getNameAndValue)
|
||||
.filter(n => n);
|
||||
|
||||
const iq = $iq({to: jid, type: "set"})
|
||||
.c("command", {
|
||||
'sessionid': cmd.sessionid,
|
||||
'node': cmd.node,
|
||||
'xmlns': Strophe.NS.ADHOC
|
||||
}).c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
|
||||
config_array.forEach(node => iq.cnode(node).up());
|
||||
const response = await api.adhoc.runCommand(jid, cmd.sessionid, cmd.node, action, inputs);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await api.sendIQ(iq);
|
||||
} catch (e) {
|
||||
const { fields, status, note, instructions, actions } = response;
|
||||
|
||||
if (status === 'error') {
|
||||
cmd.alert_type = 'danger';
|
||||
cmd.alert = __(
|
||||
'Sorry, an error occurred while trying to execute the command. See the developer console for details'
|
||||
);
|
||||
log.error('Error while trying to execute an ad-hoc command');
|
||||
log.error(e);
|
||||
return this.requestUpdate();
|
||||
}
|
||||
|
||||
if (result) {
|
||||
cmd.alert = result.querySelector('note')?.textContent;
|
||||
if (status === 'executing') {
|
||||
cmd.alert = __('Executing');
|
||||
cmd.fields = fields;
|
||||
cmd.instructions = instructions;
|
||||
cmd.alert_type = 'primary';
|
||||
cmd.actions = actions;
|
||||
} else if (status === 'completed') {
|
||||
this.alert_type = 'primary';
|
||||
this.alert = __('Completed');
|
||||
this.note = note;
|
||||
this.clearCommand(cmd);
|
||||
} else {
|
||||
cmd.alert = 'Done';
|
||||
log.error(`Unexpected status for ad-hoc command: ${status}`);
|
||||
cmd.alert = __('Completed');
|
||||
cmd.alert_type = 'primary';
|
||||
}
|
||||
cmd.alert_type = 'primary';
|
||||
this.nonce = u.getUniqueId();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async cancel (ev) {
|
||||
ev.preventDefault();
|
||||
this.showform = '';
|
||||
this.requestUpdate();
|
||||
|
||||
const form_data = new FormData(ev.target.form);
|
||||
const jid = form_data.get('command_jid').trim();
|
||||
const node = form_data.get('command_node').trim();
|
||||
|
||||
const cmd = this.commands.filter(c => c.node === node)[0];
|
||||
delete cmd.alert;
|
||||
this.requestUpdate();
|
||||
|
||||
const { status } = await api.adhoc.runCommand(jid, cmd.sessionid, cmd.node, 'cancel', []);
|
||||
|
||||
if (status === 'error') {
|
||||
cmd.alert_type = 'danger';
|
||||
cmd.alert = __(
|
||||
'An error occurred while trying to cancel the command. See the developer console for details'
|
||||
);
|
||||
} else if (status === 'canceled') {
|
||||
this.alert_type = '';
|
||||
this.alert = '';
|
||||
this.clearCommand(cmd);
|
||||
} else {
|
||||
log.error(`Unexpected status for ad-hoc command: ${status}`);
|
||||
cmd.alert = __('Error: unexpected result');
|
||||
cmd.alert_type = 'danger';
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,25 +1,41 @@
|
||||
import { __ } from 'i18n';
|
||||
import { html } from "lit";
|
||||
|
||||
export default (o, command) => {
|
||||
const i18n_hide = __('Hide');
|
||||
const i18n_run = __('Execute');
|
||||
|
||||
const action_map = {
|
||||
execute: __('Execute'),
|
||||
prev: __('Previous'),
|
||||
next: __('Next'),
|
||||
complete: __('Complete'),
|
||||
}
|
||||
|
||||
export default (el, command) => {
|
||||
const i18n_cancel = __('Cancel');
|
||||
|
||||
return html`
|
||||
<span> <!-- Don't remove this <span>,
|
||||
this is a workaround for a lit bug where a <form> cannot be removed
|
||||
if it contains an <input> with name "remove" -->
|
||||
<form @submit=${o.runCommand}>
|
||||
<form>
|
||||
${ command.alert ? html`<div class="alert alert-${command.alert_type}" role="alert">${command.alert}</div>` : '' }
|
||||
<fieldset class="form-group">
|
||||
<input type="hidden" name="command_node" value="${command.node}"/>
|
||||
<input type="hidden" name="command_jid" value="${command.jid}"/>
|
||||
|
||||
<p class="form-help">${command.instructions}</p>
|
||||
<p class="form-instructions">${command.instructions}</p>
|
||||
${ command.fields }
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<input type="submit" class="btn btn-primary" value="${i18n_run}">
|
||||
<input type="button" class="btn btn-secondary button-cancel" value="${i18n_hide}" @click=${o.hideCommandForm}>
|
||||
${ command.actions.map((action) =>
|
||||
html`<input data-action="${action}"
|
||||
@click=${(ev) => el.executeAction(ev)}
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
value="${action_map[action]}">`)
|
||||
}<input type="button"
|
||||
class="btn btn-secondary button-cancel"
|
||||
value="${i18n_cancel}"
|
||||
@click=${(ev) => el.cancel(ev)}>
|
||||
</fieldset>
|
||||
</form>
|
||||
</span>
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { html } from "lit";
|
||||
import tpl_command_form from './ad-hoc-command-form.js';
|
||||
|
||||
export default (o, command) => html`
|
||||
export default (el, command) => html`
|
||||
<li class="room-item list-group-item">
|
||||
<div class="available-chatroom d-flex flex-row">
|
||||
<a class="open-room available-room w-100"
|
||||
@click=${o.toggleCommandForm}
|
||||
@click=${(ev) => el.toggleCommandForm(ev)}
|
||||
data-command-node="${command.node}"
|
||||
data-command-jid="${command.jid}"
|
||||
data-command-name="${command.name}"
|
||||
title="${command.name}"
|
||||
href="#">${command.name || command.jid}</a>
|
||||
</div>
|
||||
${ command.node === o.showform ? tpl_command_form(o, command) : '' }
|
||||
${ command.node === el.showform ? tpl_command_form(el, command) : '' }
|
||||
</li>
|
||||
`;
|
||||
|
@ -5,7 +5,7 @@ import { getAutoCompleteList } from 'plugins/muc-views/utils.js';
|
||||
import { html } from "lit";
|
||||
|
||||
|
||||
export default (el, o) => {
|
||||
export default (el) => {
|
||||
const i18n_choose_service = __('On which entity do you want to run commands?');
|
||||
const i18n_choose_service_instructions = __(
|
||||
'Certain XMPP services and entities allow privileged users to execute ad-hoc commands on them.');
|
||||
@ -15,6 +15,8 @@ export default (el, o) => {
|
||||
const i18n_no_commands_found = __('No commands found');
|
||||
return html`
|
||||
${ el.alert ? html`<div class="alert alert-${el.alert_type}" role="alert">${el.alert}</div>` : '' }
|
||||
${ el.note ? html`<p class="form-help">${el.note}</p>` : '' }
|
||||
|
||||
<form class="converse-form" @submit=${el.fetchCommands}>
|
||||
<fieldset class="form-group">
|
||||
<label>
|
||||
@ -22,6 +24,7 @@ export default (el, o) => {
|
||||
<p class="form-help">${i18n_choose_service_instructions}</p>
|
||||
<converse-autocomplete
|
||||
.getAutoCompleteList="${getAutoCompleteList}"
|
||||
required
|
||||
placeholder="${i18n_jid_placeholder}"
|
||||
name="jid">
|
||||
</converse-autocomplete>
|
||||
@ -34,7 +37,7 @@ export default (el, o) => {
|
||||
<fieldset class="form-group">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item active">${ el.commands.length ? i18n_commands_found : i18n_no_commands_found }:</li>
|
||||
${ el.commands.map(cmd => tpl_command(o, cmd)) }
|
||||
${ el.commands.map(cmd => tpl_command(el, cmd)) }
|
||||
</ul>
|
||||
</fieldset>`
|
||||
: '' }
|
||||
|
@ -16,11 +16,8 @@ describe("Ad-hoc commands", function () {
|
||||
const adhoc_form = modal.querySelector('converse-adhoc-commands');
|
||||
await u.waitUntil(() => u.isVisible(adhoc_form));
|
||||
|
||||
const input = adhoc_form.querySelector('input[name="jid"]');
|
||||
input.value = entity_jid;
|
||||
|
||||
const submit = adhoc_form.querySelector('input[type="submit"]');
|
||||
submit.click();
|
||||
adhoc_form.querySelector('input[name="jid"]').value = entity_jid;
|
||||
adhoc_form.querySelector('input[type="submit"]').click();
|
||||
|
||||
await mock.waitUntilDiscoConfirmed(_converse, entity_jid, [], ['http://jabber.org/protocol/commands'], [], 'info');
|
||||
|
||||
@ -31,7 +28,8 @@ describe("Ad-hoc commands", function () {
|
||||
<iq type="result"
|
||||
id="${iq.getAttribute("id")}"
|
||||
to="${_converse.jid}"
|
||||
from="${entity_jid}">
|
||||
from="${entity_jid}"
|
||||
xmlns="jabber:client">
|
||||
<query xmlns="http://jabber.org/protocol/disco#items"
|
||||
node="http://jabber.org/protocol/commands">
|
||||
<item jid="${entity_jid}"
|
||||
@ -125,12 +123,443 @@ describe("Ad-hoc commands", function () {
|
||||
expect(inputs[4].getAttribute('name')).toBe('password');
|
||||
expect(inputs[4].getAttribute('type')).toBe('password');
|
||||
expect(inputs[4].getAttribute('value')).toBe('secret');
|
||||
expect(inputs[5].getAttribute('type')).toBe('submit');
|
||||
expect(inputs[5].getAttribute('value')).toBe('Execute');
|
||||
expect(inputs[5].getAttribute('type')).toBe('button');
|
||||
expect(inputs[5].getAttribute('value')).toBe('Complete');
|
||||
expect(inputs[6].getAttribute('type')).toBe('button');
|
||||
expect(inputs[6].getAttribute('value')).toBe('Hide');
|
||||
expect(inputs[6].getAttribute('value')).toBe('Cancel');
|
||||
|
||||
inputs[6].click();
|
||||
await u.waitUntil(() => !u.isVisible(form));
|
||||
}));
|
||||
});
|
||||
|
||||
describe("Ad-hoc commands consisting of multiple steps", function () {
|
||||
|
||||
beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
|
||||
|
||||
it("can be queried and executed via a modal", mock.initConverse([], {}, async (_converse) => {
|
||||
const { api } = _converse;
|
||||
const entity_jid = 'montague.lit';
|
||||
const { IQ_stanzas } = _converse.connection;
|
||||
|
||||
const modal = await api.modal.show('converse-user-settings-modal');
|
||||
await u.waitUntil(() => u.isVisible(modal));
|
||||
modal.querySelector('#commands-tab').click();
|
||||
|
||||
const adhoc_form = modal.querySelector('converse-adhoc-commands');
|
||||
await u.waitUntil(() => u.isVisible(adhoc_form));
|
||||
|
||||
adhoc_form.querySelector('input[name="jid"]').value = entity_jid;
|
||||
adhoc_form.querySelector('input[type="submit"]').click();
|
||||
|
||||
await mock.waitUntilDiscoConfirmed(_converse, entity_jid, [], ['http://jabber.org/protocol/commands'], [], 'info');
|
||||
|
||||
let sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#items"]`;
|
||||
let iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
|
||||
|
||||
expect(iq).toEqualStanza(stx`
|
||||
<iq from="${_converse.jid}" id="${iq.getAttribute('id')}" to="${entity_jid}" type="get" xmlns="jabber:client">
|
||||
<query node="http://jabber.org/protocol/commands" xmlns="http://jabber.org/protocol/disco#items"/>
|
||||
</iq>`
|
||||
);
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stx`
|
||||
<iq xmlns="jabber:client" id="${iq.getAttribute('id')}" type="result" from="${entity_jid}" to="${_converse.jid}">
|
||||
<query xmlns="http://jabber.org/protocol/disco#items" node="http://jabber.org/protocol/commands">
|
||||
<item node="uptime" name="Get uptime" jid="${entity_jid}"/>
|
||||
<item node="urn:xmpp:mam#configure" name="Archive settings" jid="${entity_jid}"/>
|
||||
<item node="xmpp:zash.se/mod_adhoc_dataforms_demo#form" name="Dataforms Demo" jid="${entity_jid}"/>
|
||||
<item node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi" name="Multi-step command demo" jid="${entity_jid}"/>
|
||||
</query>
|
||||
</iq>
|
||||
`));
|
||||
|
||||
const item = await u.waitUntil(() => adhoc_form.querySelector('form a[data-command-node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi"]'));
|
||||
item.click();
|
||||
|
||||
sel = `iq[to="${entity_jid}"] command`;
|
||||
iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
|
||||
|
||||
expect(iq).toEqualStanza(stx`
|
||||
<iq id="${iq.getAttribute('id')}" to="${entity_jid}" type="set" xmlns="jabber:client">
|
||||
<command action="execute" node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi" xmlns="http://jabber.org/protocol/commands"/>
|
||||
</iq>`
|
||||
);
|
||||
|
||||
const sessionid = "f4d477d3-d8b1-452d-95c9-fece53ef99ad";
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stx`
|
||||
<iq xmlns="jabber:client" id="${iq.getAttribute('id')}" type="result" from="${entity_jid}" to="${_converse.jid}">
|
||||
<command xmlns="http://jabber.org/protocol/commands" sessionid="${sessionid}" status="executing" node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi">
|
||||
<actions>
|
||||
<next/>
|
||||
<complete/>
|
||||
</actions>
|
||||
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<title>Step 1</title>
|
||||
<instructions>Here's a form.</instructions>
|
||||
<field label="text-private-label" type="text-private" var="text-private-field">
|
||||
<value>text-private-value</value>
|
||||
</field>
|
||||
<field label="jid-multi-label" type="jid-multi" var="jid-multi-field">
|
||||
<value>jid@multi/value#1</value>
|
||||
<value>jid@multi/value#2</value>
|
||||
</field>
|
||||
<field label="text-multi-label" type="text-multi" var="text-multi-field">
|
||||
<value>text</value>
|
||||
<value>multi</value>
|
||||
<value>value</value>
|
||||
</field>
|
||||
<field label="jid-single-label" type="jid-single" var="jid-single-field">
|
||||
<value>jid@single/value</value>
|
||||
</field>
|
||||
<field label="list-single-label" type="list-single" var="list-single-field">
|
||||
<option label="list-single-value"><value>list-single-value</value></option>
|
||||
<option label="list-single-value#2"><value>list-single-value#2</value></option>
|
||||
<option label="list-single-value#3"><value>list-single-value#3</value></option>
|
||||
<value>list-single-value</value>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>
|
||||
`));
|
||||
|
||||
let button = await u.waitUntil(() => modal.querySelector('input[data-action="next"]'));
|
||||
button.click();
|
||||
|
||||
sel = `iq[to="${entity_jid}"] command[sessionid="${sessionid}"]`;
|
||||
iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
|
||||
|
||||
expect(iq).toEqualStanza(stx`
|
||||
<iq type="set" to="${entity_jid}" xmlns="jabber:client" id="${iq.getAttribute('id')}">
|
||||
<command sessionid="${sessionid}" node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi" action="next" xmlns="http://jabber.org/protocol/commands">
|
||||
<x type="submit" xmlns="jabber:x:data">
|
||||
<field var="text-private-field">
|
||||
<value>text-private-value</value>
|
||||
</field>
|
||||
<field var="jid-multi-field">
|
||||
<value>jid@multi/value#1</value>
|
||||
</field>
|
||||
<field var="text-multi-field">
|
||||
<value>text</value>
|
||||
</field>
|
||||
<field var="jid-single-field">
|
||||
<value>jid@single/value</value>
|
||||
</field>
|
||||
<field var="list-single-field">
|
||||
<value>list-single-value</value>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>`
|
||||
);
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stx`
|
||||
<iq xmlns="jabber:client" id="${iq.getAttribute('id')}" type="result" from="${entity_jid}" to="${_converse.jid}">
|
||||
<command xmlns="http://jabber.org/protocol/commands" sessionid="${sessionid}" status="executing" node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi">
|
||||
<actions>
|
||||
<prev/>
|
||||
<next/>
|
||||
<complete/>
|
||||
</actions>
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<title>Step 2</title>
|
||||
<instructions>Here's another form.</instructions>
|
||||
<field label="jid-multi-label" type="jid-multi" var="jid-multi-field">
|
||||
<value>jid@multi/value#1</value>
|
||||
<value>jid@multi/value#2</value>
|
||||
</field>
|
||||
<field label="boolean-label" type="boolean" var="boolean-field">
|
||||
<value>1</value>
|
||||
</field>
|
||||
<field label="fixed-label" type="fixed" var="fixed-field#1">
|
||||
<value>fixed-value</value>
|
||||
</field>
|
||||
<field label="list-single-label" type="list-single" var="list-single-field">
|
||||
<option label="list-single-value">
|
||||
<value>list-single-value</value>
|
||||
</option>
|
||||
<option label="list-single-value#2">
|
||||
<value>list-single-value#2</value>
|
||||
</option>
|
||||
<option label="list-single-value#3">
|
||||
<value>list-single-value#3</value>
|
||||
</option>
|
||||
<value>list-single-value</value>
|
||||
</field>
|
||||
<field label="text-single-label" type="text-single" var="text-single-field">
|
||||
<value>text-single-value</value>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>
|
||||
`));
|
||||
|
||||
button = await u.waitUntil(() => modal.querySelector('input[data-action="complete"]'));
|
||||
button.click();
|
||||
|
||||
sel = `iq[to="${entity_jid}"] command[sessionid="${sessionid}"][action="complete"]`;
|
||||
iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
|
||||
|
||||
expect(iq).toEqualStanza(stx`
|
||||
<iq xmlns="jabber:client"
|
||||
type="set"
|
||||
to="${entity_jid}"
|
||||
id="${iq.getAttribute('id')}">
|
||||
|
||||
<command xmlns="http://jabber.org/protocol/commands"
|
||||
sessionid="${sessionid}"
|
||||
node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi"
|
||||
action="complete">
|
||||
<x xmlns="jabber:x:data"
|
||||
type="submit">
|
||||
<field var="text-private-field">
|
||||
<value>text-private-value</value></field>
|
||||
<field var="jid-multi-field"><value>jid@multi/value#1</value></field>
|
||||
<field var="text-multi-field"><value>text</value></field>
|
||||
<field var="jid-single-field"><value>jid@single/value</value></field>
|
||||
<field var="list-single-field"><value>list-single-value</value></field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>`
|
||||
);
|
||||
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stx`
|
||||
<iq xmlns="jabber:server" type="result" from="${entity_jid}" to="${_converse.jid}" id="${iq.getAttribute("id")}">
|
||||
<command xmlns="http://jabber.org/protocol/commands"
|
||||
sessionid="${sessionid}"
|
||||
node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi"
|
||||
status="completed">
|
||||
<note type="info">Service has been configured.</note>
|
||||
</command>
|
||||
</iq>`)
|
||||
);
|
||||
}));
|
||||
|
||||
it("can be canceled", mock.initConverse([], {}, async (_converse) => {
|
||||
const { api } = _converse;
|
||||
const entity_jid = 'montague.lit';
|
||||
const { IQ_stanzas } = _converse.connection;
|
||||
|
||||
const modal = await api.modal.show('converse-user-settings-modal');
|
||||
await u.waitUntil(() => u.isVisible(modal));
|
||||
modal.querySelector('#commands-tab').click();
|
||||
|
||||
const adhoc_form = modal.querySelector('converse-adhoc-commands');
|
||||
await u.waitUntil(() => u.isVisible(adhoc_form));
|
||||
|
||||
adhoc_form.querySelector('input[name="jid"]').value = entity_jid;
|
||||
adhoc_form.querySelector('input[type="submit"]').click();
|
||||
|
||||
await mock.waitUntilDiscoConfirmed(_converse, entity_jid, [], ['http://jabber.org/protocol/commands'], [], 'info');
|
||||
|
||||
let sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#items"]`;
|
||||
let iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stx`
|
||||
<iq xmlns="jabber:client" id="${iq.getAttribute('id')}" type="result" from="${entity_jid}" to="${_converse.jid}">
|
||||
<query xmlns="http://jabber.org/protocol/disco#items" node="http://jabber.org/protocol/commands">
|
||||
<item node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi" name="Multi-step command" jid="${entity_jid}"/>
|
||||
</query>
|
||||
</iq>
|
||||
`));
|
||||
|
||||
const item = await u.waitUntil(() => adhoc_form.querySelector('form a[data-command-node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi"]'));
|
||||
item.click();
|
||||
|
||||
sel = `iq[to="${entity_jid}"] command`;
|
||||
iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
|
||||
|
||||
const sessionid = "f4d477d3-d8b1-452d-95c9-fece53ef99cc";
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stx`
|
||||
<iq xmlns="jabber:client" id="${iq.getAttribute('id')}" type="result" from="${entity_jid}" to="${_converse.jid}">
|
||||
<command xmlns="http://jabber.org/protocol/commands" sessionid="${sessionid}" status="executing" node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi">
|
||||
<actions>
|
||||
<next/>
|
||||
<complete/>
|
||||
</actions>
|
||||
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<title>Step 1</title>
|
||||
<instructions>Here's a form.</instructions>
|
||||
<field label="text-private-label" type="text-private" var="text-private-field">
|
||||
<value>text-private-value</value>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>
|
||||
`));
|
||||
|
||||
const button = await u.waitUntil(() => modal.querySelector('input.button-cancel'));
|
||||
button.click();
|
||||
|
||||
sel = `iq[to="${entity_jid}"] command[sessionid="${sessionid}"]`;
|
||||
iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
|
||||
|
||||
expect(iq).toEqualStanza(stx`
|
||||
<iq type="set" to="${entity_jid}" xmlns="jabber:client" id="${iq.getAttribute('id')}">
|
||||
<command sessionid="${sessionid}"
|
||||
node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi"
|
||||
action="cancel"
|
||||
xmlns="http://jabber.org/protocol/commands">
|
||||
</command>
|
||||
</iq>`
|
||||
);
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stx`
|
||||
<iq xmlns="jabber:client" id="${iq.getAttribute('id')}" type="result" from="${entity_jid}" to="${_converse.jid}">
|
||||
<command xmlns="http://jabber.org/protocol/commands"
|
||||
sessionid="${sessionid}"
|
||||
status="canceled"
|
||||
node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi">
|
||||
</command>
|
||||
</iq>
|
||||
`));
|
||||
}));
|
||||
|
||||
it("can be navigated backwards", mock.initConverse([], {}, async (_converse) => {
|
||||
const { api } = _converse;
|
||||
const entity_jid = 'montague.lit';
|
||||
const { IQ_stanzas } = _converse.connection;
|
||||
|
||||
const modal = await api.modal.show('converse-user-settings-modal');
|
||||
await u.waitUntil(() => u.isVisible(modal));
|
||||
modal.querySelector('#commands-tab').click();
|
||||
|
||||
const adhoc_form = modal.querySelector('converse-adhoc-commands');
|
||||
await u.waitUntil(() => u.isVisible(adhoc_form));
|
||||
|
||||
adhoc_form.querySelector('input[name="jid"]').value = entity_jid;
|
||||
adhoc_form.querySelector('input[type="submit"]').click();
|
||||
|
||||
await mock.waitUntilDiscoConfirmed(_converse, entity_jid, [], ['http://jabber.org/protocol/commands'], [], 'info');
|
||||
|
||||
let sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#items"]`;
|
||||
let iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
|
||||
|
||||
expect(iq).toEqualStanza(stx`
|
||||
<iq from="${_converse.jid}" to="${entity_jid}" type="get" xmlns="jabber:client" id="${iq.getAttribute('id')}">
|
||||
<query xmlns="http://jabber.org/protocol/disco#items" node="http://jabber.org/protocol/commands"/>
|
||||
</iq>`
|
||||
);
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stx`
|
||||
<iq xmlns="jabber:client" id="${iq.getAttribute('id')}" type="result" from="${entity_jid}" to="${_converse.jid}">
|
||||
<query xmlns="http://jabber.org/protocol/disco#items" node="http://jabber.org/protocol/commands">
|
||||
<item node="uptime" name="Get uptime" jid="${entity_jid}"/>
|
||||
<item node="urn:xmpp:mam#configure" name="Archive settings" jid="${entity_jid}"/>
|
||||
<item node="xmpp:zash.se/mod_adhoc_dataforms_demo#form" name="Dataforms Demo" jid="${entity_jid}"/>
|
||||
<item node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi" name="Multi-step command demo" jid="${entity_jid}"/>
|
||||
</query>
|
||||
</iq>
|
||||
`));
|
||||
|
||||
const item = await u.waitUntil(() => adhoc_form.querySelector('form a[data-command-node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi"]'));
|
||||
item.click();
|
||||
|
||||
sel = `iq[to="${entity_jid}"] command`;
|
||||
iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
|
||||
|
||||
expect(iq).toEqualStanza(stx`
|
||||
<iq id="${iq.getAttribute('id')}" to="${entity_jid}" type="set" xmlns="jabber:client">
|
||||
<command action="execute" node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi" xmlns="http://jabber.org/protocol/commands"/>
|
||||
</iq>`);
|
||||
|
||||
const sessionid = "f4d477d3-d8b1-452d-95c9-fece53ef99ad";
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stx`
|
||||
<iq xmlns="jabber:client" id="${iq.getAttribute('id')}" type="result" from="${entity_jid}" to="${_converse.jid}">
|
||||
<command xmlns="http://jabber.org/protocol/commands" sessionid="${sessionid}" status="executing" node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi">
|
||||
<actions>
|
||||
<next/>
|
||||
<complete/>
|
||||
</actions>
|
||||
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<title>Step 1</title>
|
||||
<instructions>Here's a form.</instructions>
|
||||
<field label="text-private-label" type="text-private" var="text-private-field">
|
||||
<value>text-private-value</value>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>
|
||||
`));
|
||||
|
||||
let button = await u.waitUntil(() => modal.querySelector('input[data-action="next"]'));
|
||||
button.click();
|
||||
|
||||
sel = `iq[to="${entity_jid}"] command[sessionid="${sessionid}"]`;
|
||||
iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
|
||||
|
||||
expect(iq).toEqualStanza(stx`
|
||||
<iq type="set" to="${entity_jid}" xmlns="jabber:client" id="${iq.getAttribute('id')}">
|
||||
<command sessionid="${sessionid}" node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi" action="next" xmlns="http://jabber.org/protocol/commands">
|
||||
<x type="submit" xmlns="jabber:x:data">
|
||||
<field var="text-private-field">
|
||||
<value>text-private-value</value>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>`
|
||||
);
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stx`
|
||||
<iq xmlns="jabber:client" id="${iq.getAttribute('id')}" type="result" from="${entity_jid}" to="${_converse.jid}">
|
||||
<command xmlns="http://jabber.org/protocol/commands" sessionid="${sessionid}" status="executing" node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi">
|
||||
<actions>
|
||||
<prev/>
|
||||
<next/>
|
||||
<complete/>
|
||||
</actions>
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<title>Step 2</title>
|
||||
<instructions>Here's another form.</instructions>
|
||||
<field label="jid-multi-label" type="jid-multi" var="jid-multi-field">
|
||||
<value>jid@multi/value#1</value>
|
||||
<value>jid@multi/value#2</value>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>
|
||||
`));
|
||||
|
||||
button = await u.waitUntil(() => modal.querySelector('input[data-action="prev"]'));
|
||||
button.click();
|
||||
|
||||
sel = `iq[to="${entity_jid}"] command[sessionid="${sessionid}"][action="prev"]`;
|
||||
iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
|
||||
|
||||
expect(iq).toEqualStanza(stx`
|
||||
<iq type="set" to="${entity_jid}" xmlns="jabber:client" id="${iq.getAttribute('id')}">
|
||||
<command sessionid="${sessionid}"
|
||||
node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi"
|
||||
action="prev"
|
||||
xmlns="http://jabber.org/protocol/commands">
|
||||
</command>
|
||||
</iq>`
|
||||
);
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stx`
|
||||
<iq xmlns="jabber:client" id="${iq.getAttribute('id')}" type="result" from="${entity_jid}" to="${_converse.jid}">
|
||||
<command xmlns="http://jabber.org/protocol/commands" sessionid="${sessionid}" status="executing" node="xmpp:zash.se/mod_adhoc_dataforms_demo#multi">
|
||||
<actions>
|
||||
<next/>
|
||||
<complete/>
|
||||
</actions>
|
||||
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<title>Step 1</title>
|
||||
<instructions>Here's a form.</instructions>
|
||||
<field label="text-private-label" type="text-private" var="text-private-field">
|
||||
<value>text-private-value</value>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>
|
||||
`));
|
||||
}));
|
||||
});
|
||||
|
@ -1,34 +0,0 @@
|
||||
import log from "@converse/headless/log";
|
||||
import { api, converse } from "@converse/headless/core";
|
||||
|
||||
const { Strophe, $iq, sizzle, u } = converse.env;
|
||||
|
||||
export async function fetchCommandForm (command) {
|
||||
const node = command.node;
|
||||
const jid = command.jid;
|
||||
const stanza = $iq({
|
||||
'type': 'set',
|
||||
'to': jid
|
||||
}).c('command', {
|
||||
'xmlns': Strophe.NS.ADHOC,
|
||||
'node': node,
|
||||
'action': 'execute'
|
||||
});
|
||||
try {
|
||||
const iq = await api.sendIQ(stanza);
|
||||
const cmd_el = sizzle(`command[xmlns="${Strophe.NS.ADHOC}"]`, iq).pop();
|
||||
command.sessionid = cmd_el.getAttribute('sessionid');
|
||||
command.instructions = sizzle('x[type="form"][xmlns="jabber:x:data"] instructions', cmd_el).pop()?.textContent;
|
||||
command.fields = sizzle('x[type="form"][xmlns="jabber:x:data"] field', cmd_el)
|
||||
.map(f => u.xForm2TemplateResult(f, cmd_el, { domain: jid }));
|
||||
|
||||
} catch (e) {
|
||||
if (e === null) {
|
||||
log.error(`Error: timeout while trying to execute command for ${jid}`);
|
||||
} else {
|
||||
log.error(`Error while trying to execute command for ${jid}`);
|
||||
log.error(e);
|
||||
}
|
||||
command.fields = [];
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import tpl_form_url from "templates/form_url.js";
|
||||
import tpl_form_username from "templates/form_username.js";
|
||||
import tpl_register_panel from "./templates/register_panel.js";
|
||||
import tpl_spinner from "templates/spinner.js";
|
||||
import utils from "@converse/headless/utils/form";
|
||||
import { webForm2xForm } from "@converse/headless/utils/form";
|
||||
import { ElementView } from "@converse/skeletor/src/element";
|
||||
import { __ } from 'i18n';
|
||||
import { _converse, api, converse } from "@converse/headless/core.js";
|
||||
@ -318,7 +318,7 @@ class RegisterPanel extends ElementView {
|
||||
getFormFields (stanza) {
|
||||
if (this.form_type === 'xform') {
|
||||
return Array.from(stanza.querySelectorAll('field')).map(field =>
|
||||
utils.xForm2TemplateResult(field, stanza, {'domain': this.domain})
|
||||
u.xForm2TemplateResult(field, stanza, {'domain': this.domain})
|
||||
);
|
||||
} else {
|
||||
return this.getLegacyFormFields();
|
||||
@ -420,7 +420,7 @@ class RegisterPanel extends ElementView {
|
||||
if (this.form_type === 'xform') {
|
||||
iq.c("x", {xmlns: Strophe.NS.XFORM, type: 'submit'});
|
||||
|
||||
const xml_nodes = inputs.map(i => utils.webForm2xForm(i)).filter(n => n);
|
||||
const xml_nodes = inputs.map(i => webForm2xForm(i)).filter(n => n);
|
||||
xml_nodes.forEach(n => iq.cnode(n).up());
|
||||
} else {
|
||||
inputs.forEach(input => iq.c(input.getAttribute('name'), {}, input.value));
|
||||
|
@ -9,14 +9,26 @@
|
||||
}
|
||||
|
||||
form {
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-instructions {
|
||||
color: var(--text-color);
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.hidden-username {
|
||||
opacity: 0 !important;
|
||||
height: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.error-feedback {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
margin-top: $form-check-input-margin-y;
|
||||
}
|
||||
@ -101,17 +113,9 @@
|
||||
input[type=text] {
|
||||
min-width: 50%;
|
||||
}
|
||||
input[type=text],
|
||||
input[type=password],
|
||||
input[type=number],
|
||||
input[type=button],
|
||||
input[type=submit] {
|
||||
padding: 0.5em;
|
||||
}
|
||||
input[type=button],
|
||||
input[type=submit] {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
margin-right: 0.25em;
|
||||
border: none;
|
||||
}
|
||||
input.error {
|
||||
|
@ -4,8 +4,21 @@ const converse = window.converse;
|
||||
converse.load();
|
||||
const { u, sizzle, Strophe, dayjs, $iq, $msg, $pres } = converse.env;
|
||||
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000;
|
||||
|
||||
jasmine.toEqualStanza = function toEqualStanza () {
|
||||
return {
|
||||
compare (actual, expected) {
|
||||
const result = { pass: u.isEqualNode(actual, expected) };
|
||||
if (!result.pass) {
|
||||
result.message = `Stanzas don't match:\nActual:\n${actual.outerHTML}\nExpected:\n${expected.outerHTML}`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initConverse (promise_names=[], settings=null, func) {
|
||||
if (typeof promise_names === "function") {
|
||||
func = promise_names;
|
||||
@ -670,7 +683,7 @@ async function _initConverse (settings) {
|
||||
name[last] = name[last].charAt(0).toUpperCase()+name[last].slice(1);
|
||||
fullname = name.join(' ');
|
||||
}
|
||||
const vcard = $iq().c('vCard').c('FN').t(fullname).nodeTree;
|
||||
const vcard = $iq().c('vCard').c('FN').t(fullname).tree();
|
||||
return {
|
||||
'stanza': vcard,
|
||||
'fullname': vcard.querySelector('FN')?.textContent,
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { html } from "lit";
|
||||
import u from '@converse/headless/utils/core.js';
|
||||
|
||||
export default (o) => html`
|
||||
<label class="label-ta">${o.label}</label>
|
||||
<textarea name="${o.name}">${o.value}</textarea>
|
||||
`;
|
||||
export default (o) => {
|
||||
const id = u.getUniqueId();
|
||||
return html`
|
||||
<div class="form-group">
|
||||
<label class="label-ta" for="${id}">${o.label}</label>
|
||||
<textarea name="${o.name}" id="${id}" class="form-control">${o.value}</textarea>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ import { converse } from '@converse/headless/core';
|
||||
import { getURI, isAudioURL, isImageURL, isVideoURL } from '@converse/headless/utils/url.js';
|
||||
import { render } from 'lit';
|
||||
|
||||
const { sizzle } = converse.env;
|
||||
const { sizzle, Strophe } = converse.env;
|
||||
|
||||
const APPROVED_URL_PROTOCOLS = ['http', 'https', 'xmpp', 'mailto'];
|
||||
|
||||
@ -54,6 +54,93 @@ const XFORM_VALIDATE_TYPE_MAP = {
|
||||
'xs:time': 'time',
|
||||
}
|
||||
|
||||
|
||||
const EMPTY_TEXT_REGEX = /\s*\n\s*/
|
||||
|
||||
function stripEmptyTextNodes (el) {
|
||||
el = el.tree?.() ?? el;
|
||||
|
||||
let n;
|
||||
const text_nodes = [];
|
||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, (node) => {
|
||||
if (node.parentElement.nodeName.toLowerCase() === 'body') {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
});
|
||||
while (n = walker.nextNode()) text_nodes.push(n);
|
||||
text_nodes.forEach((n) => EMPTY_TEXT_REGEX.test(n.data) && n.parentElement.removeChild(n))
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
const serializer = new XMLSerializer();
|
||||
|
||||
/**
|
||||
* Given two XML or HTML elements, determine if they're equal
|
||||
* @param { XMLElement | HTMLElement } actual
|
||||
* @param { XMLElement | HTMLElement } expected
|
||||
* @returns { Boolean }
|
||||
*/
|
||||
function isEqualNode (actual, expected) {
|
||||
if (!u.isElement(actual)) throw new Error("Element being compared must be an Element!");
|
||||
|
||||
actual = stripEmptyTextNodes(actual);
|
||||
expected = stripEmptyTextNodes(expected);
|
||||
|
||||
let isEqual = actual.isEqualNode(expected);
|
||||
|
||||
if (!isEqual) {
|
||||
// XXX: This is a hack.
|
||||
// When creating two XML elements, one via DOMParser, and one via
|
||||
// createElementNS (or createElement), then "isEqualNode" doesn't match.
|
||||
//
|
||||
// For example, in the following code `isEqual` is false:
|
||||
// ------------------------------------------------------
|
||||
// const a = document.createElementNS('foo', 'div');
|
||||
// a.setAttribute('xmlns', 'foo');
|
||||
//
|
||||
// const b = (new DOMParser()).parseFromString('<div xmlns="foo"></div>', 'text/xml').firstElementChild;
|
||||
// const isEqual = a.isEqualNode(div); // false
|
||||
//
|
||||
// The workaround here is to serialize both elements to string and then use
|
||||
// DOMParser again for both (via xmlHtmlNode).
|
||||
//
|
||||
// This is not efficient, but currently this is only being used in tests.
|
||||
//
|
||||
const { xmlHtmlNode } = Strophe;
|
||||
const actual_string = serializer.serializeToString(actual);
|
||||
const expected_string = serializer.serializeToString(expected);
|
||||
isEqual = actual_string === expected_string || xmlHtmlNode(actual_string).isEqualNode(xmlHtmlNode(expected_string));
|
||||
}
|
||||
|
||||
return isEqual;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an HTMLElement representing a form field, return it's name and value.
|
||||
* @param { HTMLElement } field
|
||||
* @returns { { string, string } | null }
|
||||
*/
|
||||
export function getNameAndValue(field) {
|
||||
const name = field.getAttribute('name');
|
||||
if (!name) {
|
||||
return null; // See #1924
|
||||
}
|
||||
let value;
|
||||
if (field.getAttribute('type') === 'checkbox') {
|
||||
value = field.checked && 1 || 0;
|
||||
} else if (field.tagName == "TEXTAREA") {
|
||||
value = field.value.split('\n').filter(s => s.trim());
|
||||
} else if (field.tagName == "SELECT") {
|
||||
value = u.getSelectValues(field);
|
||||
} else {
|
||||
value = field.value;
|
||||
}
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
|
||||
function getInputType(field) {
|
||||
const type = XFORM_TYPE_MAP[field.getAttribute('type')]
|
||||
if (type == 'text') {
|
||||
@ -525,6 +612,6 @@ u.xForm2TemplateResult = function (field, stanza, options={}) {
|
||||
}
|
||||
};
|
||||
|
||||
Object.assign(u, { getOOBURLMarkup, ancestor, slideIn, slideOut });
|
||||
Object.assign(u, { getOOBURLMarkup, ancestor, slideIn, slideOut, isEqualNode });
|
||||
|
||||
export default u;
|
||||
|
@ -21,6 +21,7 @@
|
||||
}
|
||||
});
|
||||
converse.initialize({
|
||||
reuse_scram_keys: true,
|
||||
muc_subscribe_to_rai: true,
|
||||
theme: 'dracula',
|
||||
show_send_button: true,
|
||||
|
Loading…
Reference in New Issue
Block a user