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)
|
## 10.1.1 (Unreleased)
|
||||||
|
|
||||||
|
- #2240: Ad-Hoc command result form not shown
|
||||||
- #3128: Second bookmarked room shows info of the first one
|
- #3128: Second bookmarked room shows info of the first one
|
||||||
- Bugfix. Uyghur translations weren't loading
|
- 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.
|
* nothing to wait for, so an already resolved promise is returned.
|
||||||
*/
|
*/
|
||||||
sendIQ (stanza, timeout=_converse.STANZA_TIMEOUT, reject=true) {
|
sendIQ (stanza, timeout=_converse.STANZA_TIMEOUT, reject=true) {
|
||||||
|
|
||||||
|
const { connection } = _converse;
|
||||||
|
|
||||||
let promise;
|
let promise;
|
||||||
stanza = stanza.tree?.() ?? stanza;
|
stanza = stanza.tree?.() ?? stanza;
|
||||||
if (['get', 'set'].includes(stanza.getAttribute('type'))) {
|
if (['get', 'set'].includes(stanza.getAttribute('type'))) {
|
||||||
timeout = timeout || _converse.STANZA_TIMEOUT;
|
timeout = timeout || _converse.STANZA_TIMEOUT;
|
||||||
if (reject) {
|
if (reject) {
|
||||||
promise = new Promise((resolve, reject) => _converse.connection.sendIQ(stanza, resolve, reject, timeout));
|
promise = new Promise((resolve, reject) => connection.sendIQ(stanza, resolve, reject, timeout));
|
||||||
promise.catch(e => {
|
promise.catch((e) => {
|
||||||
if (e === null) {
|
if (e === null) {
|
||||||
throw new TimeoutError(
|
throw new TimeoutError(
|
||||||
`Timeout error after ${timeout}ms for the following IQ stanza: ${Strophe.serialize(stanza)}`
|
`Timeout error after ${timeout}ms for the following IQ stanza: ${Strophe.serialize(stanza)}`
|
||||||
@ -467,7 +470,7 @@ export const api = _converse.api = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
promise = new Promise(resolve => _converse.connection.sendIQ(stanza, resolve, resolve, timeout));
|
promise = new Promise((resolve) => connection.sendIQ(stanza, resolve, resolve, timeout));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_converse.connection.sendIQ(stanza);
|
_converse.connection.sendIQ(stanza);
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { converse } from "../core.js";
|
|
||||||
import log from "@converse/headless/log";
|
import log from "@converse/headless/log";
|
||||||
import sizzle from 'sizzle';
|
import sizzle from 'sizzle';
|
||||||
|
import { __ } from 'i18n';
|
||||||
|
import { converse } from "../core.js";
|
||||||
import { getAttributes } from '@converse/headless/shared/parsers';
|
import { getAttributes } from '@converse/headless/shared/parsers';
|
||||||
|
|
||||||
const { Strophe } = converse.env;
|
const { Strophe, u, stx, $iq } = converse.env;
|
||||||
let _converse, api;
|
let api;
|
||||||
|
|
||||||
Strophe.addNamespace('ADHOC', 'http://jabber.org/protocol/commands');
|
Strophe.addNamespace('ADHOC', 'http://jabber.org/protocol/commands');
|
||||||
|
|
||||||
@ -14,6 +15,18 @@ function parseForCommands (stanza) {
|
|||||||
return items.map(getAttributes)
|
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 = {
|
const adhoc_api = {
|
||||||
/**
|
/**
|
||||||
@ -30,9 +43,8 @@ const adhoc_api = {
|
|||||||
* @param { String } to_jid
|
* @param { String } to_jid
|
||||||
*/
|
*/
|
||||||
async getCommands (to_jid) {
|
async getCommands (to_jid) {
|
||||||
let commands = [];
|
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
if (e === null) {
|
if (e === null) {
|
||||||
log.error(`Error: timeout while fetching ad-hoc commands for ${to_jid}`);
|
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(`Error while fetching ad-hoc commands for ${to_jid}`);
|
||||||
log.error(e);
|
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"],
|
dependencies: ["converse-disco"],
|
||||||
|
|
||||||
initialize () {
|
initialize () {
|
||||||
_converse = this._converse;
|
const _converse = this._converse;
|
||||||
api = _converse.api;
|
api = _converse.api;
|
||||||
Object.assign(api, adhoc_api);
|
Object.assign(api, adhoc_api);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import log from '../../log';
|
import log from '../../log';
|
||||||
import u from '../../utils/form';
|
|
||||||
import { Strophe } from 'strophe.js/src/strophe';
|
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 {
|
export default {
|
||||||
|
@ -5,7 +5,6 @@ import log from '../../log';
|
|||||||
import p from '../../utils/parse-helpers';
|
import p from '../../utils/parse-helpers';
|
||||||
import pick from 'lodash-es/pick';
|
import pick from 'lodash-es/pick';
|
||||||
import sizzle from 'sizzle';
|
import sizzle from 'sizzle';
|
||||||
import u from '../../utils/form';
|
|
||||||
import { Model } from '@converse/skeletor/src/model.js';
|
import { Model } from '@converse/skeletor/src/model.js';
|
||||||
import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe';
|
import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe';
|
||||||
import { _converse, api, converse } from '../../core.js';
|
import { _converse, api, converse } from '../../core.js';
|
||||||
@ -19,6 +18,8 @@ import { parseMUCMessage, parseMUCPresence } from './parsers.js';
|
|||||||
import { sendMarker } from '../../shared/actions.js';
|
import { sendMarker } from '../../shared/actions.js';
|
||||||
import { ROOMSTATUS } from './constants.js';
|
import { ROOMSTATUS } from './constants.js';
|
||||||
|
|
||||||
|
const { u } = converse.env;
|
||||||
|
|
||||||
const OWNER_COMMANDS = ['owner'];
|
const OWNER_COMMANDS = ['owner'];
|
||||||
const ADMIN_COMMANDS = ['admin', 'ban', 'deop', 'destroy', 'member', 'op', 'revoke'];
|
const ADMIN_COMMANDS = ['admin', 'ban', 'deop', 'destroy', 'member', 'op', 'revoke'];
|
||||||
const MODERATOR_COMMANDS = ['kick', 'mute', 'voice', 'modtools'];
|
const MODERATOR_COMMANDS = ['kick', 'mute', 'voice', 'modtools'];
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import ChatRoomOccupant from './occupant.js';
|
import ChatRoomOccupant from './occupant.js';
|
||||||
import u from '../../utils/form';
|
|
||||||
import { Collection } from '@converse/skeletor/src/collection.js';
|
import { Collection } from '@converse/skeletor/src/collection.js';
|
||||||
import { MUC_ROLE_WEIGHTS } from './constants.js';
|
import { MUC_ROLE_WEIGHTS } from './constants.js';
|
||||||
import { Model } from '@converse/skeletor/src/model.js';
|
import { Model } from '@converse/skeletor/src/model.js';
|
||||||
import { Strophe } from 'strophe.js/src/strophe.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 { getAffiliationList } from './affiliations/utils.js';
|
||||||
import { getAutoFetchedAffiliationLists } from './utils.js';
|
import { getAutoFetchedAffiliationLists } from './utils.js';
|
||||||
import { getUniqueId } from '@converse/headless/utils/core.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.
|
* A list of {@link _converse.ChatRoomOccupant} instances, representing participants in a MUC.
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import _converse from '@converse/headless/shared/_converse.js';
|
import _converse from '@converse/headless/shared/_converse.js';
|
||||||
import compact from "lodash-es/compact";
|
import compact from "lodash-es/compact";
|
||||||
import isElement from "lodash-es/isElement";
|
|
||||||
import isObject from "lodash-es/isObject";
|
import isObject from "lodash-es/isObject";
|
||||||
import last from "lodash-es/last";
|
import last from "lodash-es/last";
|
||||||
import log from '@converse/headless/log.js';
|
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 { settings_api } from '@converse/headless/shared/settings/api.js';
|
||||||
import { stx , toStanza } from './stanza.js';
|
import { stx , toStanza } from './stanza.js';
|
||||||
|
|
||||||
|
export function isElement (el) {
|
||||||
|
return el instanceof Element || el instanceof HTMLDocument;
|
||||||
|
}
|
||||||
|
|
||||||
export function isError (obj) {
|
export function isError (obj) {
|
||||||
return Object.prototype.toString.call(obj) === "[object Error]";
|
return Object.prototype.toString.call(obj) === "[object Error]";
|
||||||
}
|
}
|
||||||
@ -619,6 +622,7 @@ export function saveWindowState (ev) {
|
|||||||
export default Object.assign({
|
export default Object.assign({
|
||||||
getRandomInt,
|
getRandomInt,
|
||||||
getUniqueId,
|
getUniqueId,
|
||||||
|
isElement,
|
||||||
isEmptyMessage,
|
isEmptyMessage,
|
||||||
isValidJID,
|
isValidJID,
|
||||||
merge,
|
merge,
|
||||||
|
@ -5,13 +5,17 @@
|
|||||||
*/
|
*/
|
||||||
import u from "./core";
|
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.
|
* Takes an HTML DOM and turns it into an XForm field.
|
||||||
* @private
|
* @private
|
||||||
* @method u#webForm2xForm
|
* @method u#webForm2xForm
|
||||||
* @param { DOMElement } field - the field to convert
|
* @param { DOMElement } field - the field to convert
|
||||||
*/
|
*/
|
||||||
u.webForm2xForm = function (field) {
|
export function webForm2xForm (field) {
|
||||||
const name = field.getAttribute('name');
|
const name = field.getAttribute('name');
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return null; // See #1924
|
return null; // See #1924
|
||||||
@ -26,11 +30,10 @@ u.webForm2xForm = function (field) {
|
|||||||
} else {
|
} else {
|
||||||
value = field.value;
|
value = field.value;
|
||||||
}
|
}
|
||||||
return u.toStanza(`
|
return u.toStanza(tpl_xform_field(
|
||||||
<field var="${name}">
|
name,
|
||||||
${ value.constructor === Array ?
|
Array.isArray(value) ? value.map(tpl_xform_value) : tpl_xform_value(value),
|
||||||
value.map(v => `<value>${v}</value>`) :
|
));
|
||||||
`<value>${value}</value>` }
|
}
|
||||||
</field>`);
|
|
||||||
};
|
u.webForm2xForm = webForm2xForm;
|
||||||
export default u;
|
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
import 'shared/autocomplete/index.js';
|
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 tpl_adhoc from './templates/ad-hoc.js';
|
||||||
import { CustomElement } from 'shared/components/element.js';
|
import { CustomElement } from 'shared/components/element.js';
|
||||||
import { __ } from 'i18n';
|
import { __ } from 'i18n';
|
||||||
import { api, converse } from "@converse/headless/core";
|
import { api, converse } from '@converse/headless/core.js';
|
||||||
import { fetchCommandForm } from './utils.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 {
|
export default class AdHocCommands extends CustomElement {
|
||||||
|
|
||||||
static get properties () {
|
static get properties () {
|
||||||
return {
|
return {
|
||||||
'alert': { type: String },
|
'alert': { type: String },
|
||||||
'alert_type': { type: String },
|
'alert_type': { type: String },
|
||||||
'nonce': { type: String }, // Used to force re-rendering
|
'commands': { type: Array },
|
||||||
'fetching': { type: Boolean }, // Used to force re-rendering
|
'fetching': { type: Boolean },
|
||||||
'showform': { type: String },
|
'showform': { type: String },
|
||||||
'view': { type: String },
|
'view': { type: String },
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
@ -31,12 +30,7 @@ export default class AdHocCommands extends CustomElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return tpl_adhoc(this, {
|
return tpl_adhoc(this)
|
||||||
'hideCommandForm': ev => this.hideCommandForm(ev),
|
|
||||||
'runCommand': ev => this.runCommand(ev),
|
|
||||||
'showform': this.showform,
|
|
||||||
'toggleCommandForm': ev => this.toggleCommandForm(ev),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchCommands (ev) {
|
async fetchCommands (ev) {
|
||||||
@ -50,7 +44,7 @@ export default class AdHocCommands extends CustomElement {
|
|||||||
const jid = form_data.get('jid').trim();
|
const jid = form_data.get('jid').trim();
|
||||||
let supported;
|
let supported;
|
||||||
try {
|
try {
|
||||||
supported = await api.disco.supports(Strophe.NS.ADHOC, jid)
|
supported = await api.disco.supports(Strophe.NS.ADHOC, jid);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error(e);
|
log.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
@ -79,59 +73,117 @@ export default class AdHocCommands extends CustomElement {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const node = ev.target.getAttribute('data-command-node');
|
const node = ev.target.getAttribute('data-command-node');
|
||||||
const cmd = this.commands.filter(c => c.node === node)[0];
|
const cmd = this.commands.filter(c => c.node === node)[0];
|
||||||
this.showform !== node && await fetchCommandForm(cmd);
|
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;
|
this.showform = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
hideCommandForm (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.nonce = u.getUniqueId();
|
|
||||||
this.showform = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async runCommand (ev) {
|
executeAction (ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const form_data = new FormData(ev.target);
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 jid = form_data.get('command_jid').trim();
|
||||||
const node = form_data.get('command_node').trim();
|
const node = form_data.get('command_node').trim();
|
||||||
|
|
||||||
const cmd = this.commands.filter(c => c.node === node)[0];
|
const cmd = this.commands.filter(c => c.node === node)[0];
|
||||||
cmd.alert = null;
|
delete cmd.alert;
|
||||||
this.nonce = u.getUniqueId();
|
this.requestUpdate();
|
||||||
|
|
||||||
const inputs = sizzle(':input:not([type=button]):not([type=submit])', ev.target);
|
const inputs = action === 'prev' ? [] :
|
||||||
const config_array = inputs
|
sizzle(':input:not([type=button]):not([type=submit])', form)
|
||||||
.filter(i => !['command_jid', 'command_node'].includes(i.getAttribute('name')))
|
.filter(i => !['command_jid', 'command_node'].includes(i.getAttribute('name')))
|
||||||
.map(u.webForm2xForm)
|
.map(getNameAndValue)
|
||||||
.filter(n => n);
|
.filter(n => n);
|
||||||
|
|
||||||
const iq = $iq({to: jid, type: "set"})
|
const response = await api.adhoc.runCommand(jid, cmd.sessionid, cmd.node, action, inputs);
|
||||||
.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());
|
|
||||||
|
|
||||||
let result;
|
const { fields, status, note, instructions, actions } = response;
|
||||||
try {
|
|
||||||
result = await api.sendIQ(iq);
|
if (status === 'error') {
|
||||||
} catch (e) {
|
|
||||||
cmd.alert_type = 'danger';
|
cmd.alert_type = 'danger';
|
||||||
cmd.alert = __(
|
cmd.alert = __(
|
||||||
'Sorry, an error occurred while trying to execute the command. See the developer console for details'
|
'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');
|
return this.requestUpdate();
|
||||||
log.error(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result) {
|
if (status === 'executing') {
|
||||||
cmd.alert = result.querySelector('note')?.textContent;
|
cmd.alert = __('Executing');
|
||||||
} else {
|
cmd.fields = fields;
|
||||||
cmd.alert = 'Done';
|
cmd.instructions = instructions;
|
||||||
}
|
|
||||||
cmd.alert_type = 'primary';
|
cmd.alert_type = 'primary';
|
||||||
this.nonce = u.getUniqueId();
|
cmd.actions = actions;
|
||||||
|
} else if (status === 'completed') {
|
||||||
|
this.alert_type = 'primary';
|
||||||
|
this.alert = __('Completed');
|
||||||
|
this.note = note;
|
||||||
|
this.clearCommand(cmd);
|
||||||
|
} else {
|
||||||
|
log.error(`Unexpected status for ad-hoc command: ${status}`);
|
||||||
|
cmd.alert = __('Completed');
|
||||||
|
cmd.alert_type = 'primary';
|
||||||
|
}
|
||||||
|
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 { __ } from 'i18n';
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
export default (o, command) => {
|
|
||||||
const i18n_hide = __('Hide');
|
const action_map = {
|
||||||
const i18n_run = __('Execute');
|
execute: __('Execute'),
|
||||||
|
prev: __('Previous'),
|
||||||
|
next: __('Next'),
|
||||||
|
complete: __('Complete'),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (el, command) => {
|
||||||
|
const i18n_cancel = __('Cancel');
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<span> <!-- Don't remove this <span>,
|
<span> <!-- Don't remove this <span>,
|
||||||
this is a workaround for a lit bug where a <form> cannot be removed
|
this is a workaround for a lit bug where a <form> cannot be removed
|
||||||
if it contains an <input> with name "remove" -->
|
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>` : '' }
|
${ command.alert ? html`<div class="alert alert-${command.alert_type}" role="alert">${command.alert}</div>` : '' }
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
<input type="hidden" name="command_node" value="${command.node}"/>
|
<input type="hidden" name="command_node" value="${command.node}"/>
|
||||||
<input type="hidden" name="command_jid" value="${command.jid}"/>
|
<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 }
|
${ command.fields }
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<input type="submit" class="btn btn-primary" value="${i18n_run}">
|
${ command.actions.map((action) =>
|
||||||
<input type="button" class="btn btn-secondary button-cancel" value="${i18n_hide}" @click=${o.hideCommandForm}>
|
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>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
import tpl_command_form from './ad-hoc-command-form.js';
|
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">
|
<li class="room-item list-group-item">
|
||||||
<div class="available-chatroom d-flex flex-row">
|
<div class="available-chatroom d-flex flex-row">
|
||||||
<a class="open-room available-room w-100"
|
<a class="open-room available-room w-100"
|
||||||
@click=${o.toggleCommandForm}
|
@click=${(ev) => el.toggleCommandForm(ev)}
|
||||||
data-command-node="${command.node}"
|
data-command-node="${command.node}"
|
||||||
data-command-jid="${command.jid}"
|
data-command-jid="${command.jid}"
|
||||||
data-command-name="${command.name}"
|
data-command-name="${command.name}"
|
||||||
title="${command.name}"
|
title="${command.name}"
|
||||||
href="#">${command.name || command.jid}</a>
|
href="#">${command.name || command.jid}</a>
|
||||||
</div>
|
</div>
|
||||||
${ command.node === o.showform ? tpl_command_form(o, command) : '' }
|
${ command.node === el.showform ? tpl_command_form(el, command) : '' }
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
|
@ -5,7 +5,7 @@ import { getAutoCompleteList } from 'plugins/muc-views/utils.js';
|
|||||||
import { html } from "lit";
|
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 = __('On which entity do you want to run commands?');
|
||||||
const i18n_choose_service_instructions = __(
|
const i18n_choose_service_instructions = __(
|
||||||
'Certain XMPP services and entities allow privileged users to execute ad-hoc commands on them.');
|
'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');
|
const i18n_no_commands_found = __('No commands found');
|
||||||
return html`
|
return html`
|
||||||
${ el.alert ? html`<div class="alert alert-${el.alert_type}" role="alert">${el.alert}</div>` : '' }
|
${ 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}>
|
<form class="converse-form" @submit=${el.fetchCommands}>
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
<label>
|
<label>
|
||||||
@ -22,6 +24,7 @@ export default (el, o) => {
|
|||||||
<p class="form-help">${i18n_choose_service_instructions}</p>
|
<p class="form-help">${i18n_choose_service_instructions}</p>
|
||||||
<converse-autocomplete
|
<converse-autocomplete
|
||||||
.getAutoCompleteList="${getAutoCompleteList}"
|
.getAutoCompleteList="${getAutoCompleteList}"
|
||||||
|
required
|
||||||
placeholder="${i18n_jid_placeholder}"
|
placeholder="${i18n_jid_placeholder}"
|
||||||
name="jid">
|
name="jid">
|
||||||
</converse-autocomplete>
|
</converse-autocomplete>
|
||||||
@ -34,7 +37,7 @@ export default (el, o) => {
|
|||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<li class="list-group-item active">${ el.commands.length ? i18n_commands_found : i18n_no_commands_found }:</li>
|
<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>
|
</ul>
|
||||||
</fieldset>`
|
</fieldset>`
|
||||||
: '' }
|
: '' }
|
||||||
|
@ -16,11 +16,8 @@ describe("Ad-hoc commands", function () {
|
|||||||
const adhoc_form = modal.querySelector('converse-adhoc-commands');
|
const adhoc_form = modal.querySelector('converse-adhoc-commands');
|
||||||
await u.waitUntil(() => u.isVisible(adhoc_form));
|
await u.waitUntil(() => u.isVisible(adhoc_form));
|
||||||
|
|
||||||
const input = adhoc_form.querySelector('input[name="jid"]');
|
adhoc_form.querySelector('input[name="jid"]').value = entity_jid;
|
||||||
input.value = entity_jid;
|
adhoc_form.querySelector('input[type="submit"]').click();
|
||||||
|
|
||||||
const submit = adhoc_form.querySelector('input[type="submit"]');
|
|
||||||
submit.click();
|
|
||||||
|
|
||||||
await mock.waitUntilDiscoConfirmed(_converse, entity_jid, [], ['http://jabber.org/protocol/commands'], [], 'info');
|
await mock.waitUntilDiscoConfirmed(_converse, entity_jid, [], ['http://jabber.org/protocol/commands'], [], 'info');
|
||||||
|
|
||||||
@ -31,7 +28,8 @@ describe("Ad-hoc commands", function () {
|
|||||||
<iq type="result"
|
<iq type="result"
|
||||||
id="${iq.getAttribute("id")}"
|
id="${iq.getAttribute("id")}"
|
||||||
to="${_converse.jid}"
|
to="${_converse.jid}"
|
||||||
from="${entity_jid}">
|
from="${entity_jid}"
|
||||||
|
xmlns="jabber:client">
|
||||||
<query xmlns="http://jabber.org/protocol/disco#items"
|
<query xmlns="http://jabber.org/protocol/disco#items"
|
||||||
node="http://jabber.org/protocol/commands">
|
node="http://jabber.org/protocol/commands">
|
||||||
<item jid="${entity_jid}"
|
<item jid="${entity_jid}"
|
||||||
@ -125,12 +123,443 @@ describe("Ad-hoc commands", function () {
|
|||||||
expect(inputs[4].getAttribute('name')).toBe('password');
|
expect(inputs[4].getAttribute('name')).toBe('password');
|
||||||
expect(inputs[4].getAttribute('type')).toBe('password');
|
expect(inputs[4].getAttribute('type')).toBe('password');
|
||||||
expect(inputs[4].getAttribute('value')).toBe('secret');
|
expect(inputs[4].getAttribute('value')).toBe('secret');
|
||||||
expect(inputs[5].getAttribute('type')).toBe('submit');
|
expect(inputs[5].getAttribute('type')).toBe('button');
|
||||||
expect(inputs[5].getAttribute('value')).toBe('Execute');
|
expect(inputs[5].getAttribute('value')).toBe('Complete');
|
||||||
expect(inputs[6].getAttribute('type')).toBe('button');
|
expect(inputs[6].getAttribute('type')).toBe('button');
|
||||||
expect(inputs[6].getAttribute('value')).toBe('Hide');
|
expect(inputs[6].getAttribute('value')).toBe('Cancel');
|
||||||
|
|
||||||
inputs[6].click();
|
inputs[6].click();
|
||||||
await u.waitUntil(() => !u.isVisible(form));
|
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_form_username from "templates/form_username.js";
|
||||||
import tpl_register_panel from "./templates/register_panel.js";
|
import tpl_register_panel from "./templates/register_panel.js";
|
||||||
import tpl_spinner from "templates/spinner.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 { ElementView } from "@converse/skeletor/src/element";
|
||||||
import { __ } from 'i18n';
|
import { __ } from 'i18n';
|
||||||
import { _converse, api, converse } from "@converse/headless/core.js";
|
import { _converse, api, converse } from "@converse/headless/core.js";
|
||||||
@ -318,7 +318,7 @@ class RegisterPanel extends ElementView {
|
|||||||
getFormFields (stanza) {
|
getFormFields (stanza) {
|
||||||
if (this.form_type === 'xform') {
|
if (this.form_type === 'xform') {
|
||||||
return Array.from(stanza.querySelectorAll('field')).map(field =>
|
return Array.from(stanza.querySelectorAll('field')).map(field =>
|
||||||
utils.xForm2TemplateResult(field, stanza, {'domain': this.domain})
|
u.xForm2TemplateResult(field, stanza, {'domain': this.domain})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return this.getLegacyFormFields();
|
return this.getLegacyFormFields();
|
||||||
@ -420,7 +420,7 @@ class RegisterPanel extends ElementView {
|
|||||||
if (this.form_type === 'xform') {
|
if (this.form_type === 'xform') {
|
||||||
iq.c("x", {xmlns: Strophe.NS.XFORM, type: 'submit'});
|
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());
|
xml_nodes.forEach(n => iq.cnode(n).up());
|
||||||
} else {
|
} else {
|
||||||
inputs.forEach(input => iq.c(input.getAttribute('name'), {}, input.value));
|
inputs.forEach(input => iq.c(input.getAttribute('name'), {}, input.value));
|
||||||
|
@ -9,14 +9,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-instructions {
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden-username {
|
.hidden-username {
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
height: 0 !important;
|
height: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-feedback {
|
.error-feedback {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-check-label {
|
.form-check-label {
|
||||||
margin-top: $form-check-input-margin-y;
|
margin-top: $form-check-input-margin-y;
|
||||||
}
|
}
|
||||||
@ -101,17 +113,9 @@
|
|||||||
input[type=text] {
|
input[type=text] {
|
||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
}
|
}
|
||||||
input[type=text],
|
|
||||||
input[type=password],
|
|
||||||
input[type=number],
|
|
||||||
input[type=button],
|
input[type=button],
|
||||||
input[type=submit] {
|
input[type=submit] {
|
||||||
padding: 0.5em;
|
margin-right: 0.25em;
|
||||||
}
|
|
||||||
input[type=button],
|
|
||||||
input[type=submit] {
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
input.error {
|
input.error {
|
||||||
|
@ -4,8 +4,21 @@ const converse = window.converse;
|
|||||||
converse.load();
|
converse.load();
|
||||||
const { u, sizzle, Strophe, dayjs, $iq, $msg, $pres } = converse.env;
|
const { u, sizzle, Strophe, dayjs, $iq, $msg, $pres } = converse.env;
|
||||||
|
|
||||||
|
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000;
|
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) {
|
function initConverse (promise_names=[], settings=null, func) {
|
||||||
if (typeof promise_names === "function") {
|
if (typeof promise_names === "function") {
|
||||||
func = promise_names;
|
func = promise_names;
|
||||||
@ -670,7 +683,7 @@ async function _initConverse (settings) {
|
|||||||
name[last] = name[last].charAt(0).toUpperCase()+name[last].slice(1);
|
name[last] = name[last].charAt(0).toUpperCase()+name[last].slice(1);
|
||||||
fullname = name.join(' ');
|
fullname = name.join(' ');
|
||||||
}
|
}
|
||||||
const vcard = $iq().c('vCard').c('FN').t(fullname).nodeTree;
|
const vcard = $iq().c('vCard').c('FN').t(fullname).tree();
|
||||||
return {
|
return {
|
||||||
'stanza': vcard,
|
'stanza': vcard,
|
||||||
'fullname': vcard.querySelector('FN')?.textContent,
|
'fullname': vcard.querySelector('FN')?.textContent,
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
import u from '@converse/headless/utils/core.js';
|
||||||
|
|
||||||
export default (o) => html`
|
export default (o) => {
|
||||||
<label class="label-ta">${o.label}</label>
|
const id = u.getUniqueId();
|
||||||
<textarea name="${o.name}">${o.value}</textarea>
|
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 { getURI, isAudioURL, isImageURL, isVideoURL } from '@converse/headless/utils/url.js';
|
||||||
import { render } from 'lit';
|
import { render } from 'lit';
|
||||||
|
|
||||||
const { sizzle } = converse.env;
|
const { sizzle, Strophe } = converse.env;
|
||||||
|
|
||||||
const APPROVED_URL_PROTOCOLS = ['http', 'https', 'xmpp', 'mailto'];
|
const APPROVED_URL_PROTOCOLS = ['http', 'https', 'xmpp', 'mailto'];
|
||||||
|
|
||||||
@ -54,6 +54,93 @@ const XFORM_VALIDATE_TYPE_MAP = {
|
|||||||
'xs:time': 'time',
|
'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) {
|
function getInputType(field) {
|
||||||
const type = XFORM_TYPE_MAP[field.getAttribute('type')]
|
const type = XFORM_TYPE_MAP[field.getAttribute('type')]
|
||||||
if (type == 'text') {
|
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;
|
export default u;
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
converse.initialize({
|
converse.initialize({
|
||||||
|
reuse_scram_keys: true,
|
||||||
muc_subscribe_to_rai: true,
|
muc_subscribe_to_rai: true,
|
||||||
theme: 'dracula',
|
theme: 'dracula',
|
||||||
show_send_button: true,
|
show_send_button: true,
|
||||||
|
Loading…
Reference in New Issue
Block a user