Playwright Tests / test (push) Canceled after 0s
serializeObject() returns null for an invalid form, but most callers sent
the result anyway. Add $.fn.rejectInvalid (stop the spinner, let the
browser report the offending field) and use it at every call site.
Bugs fixed:
- Password change: on a mismatch the spinner kept running, and the custom
validity on "npw" was never cleared, so the browser blocked every later
submit even after the passwords matched. It is now cleared as soon as
either password field is edited.
- sendForm: options.complete referenced an undefined jqXHR (ReferenceError)
and an invalid form was posted with partial data.
- login.send: an invalid form threw a TypeError on qs.loginaccount.
- postXT({ form }): an invalid form was posted with empty data.
- Form dialogs without required fields had no "Bitte überprüfen Sie Ihre
Eingaben" note, so a rejected submit showed nothing.
Hardening: null guards for the user, user-group, re-login, SMS password,
admin specialties and report-parameter submits.
Add e2e_tests/form-validation.cjs covering each path against the real
scripts; it fails on the previous code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
5.3 KiB
JavaScript
100 lines
5.3 KiB
JavaScript
/* "Specialties" — admin-only page (URL /specialties, menu Administration → Spezialfunktionen;
|
|
the menu entry is only visible when the dropdown is opened with Shift held, see fis_main_menu.js)
|
|
that exposes the otherwise hidden maintenance actions as cards with small forms. Each card
|
|
calls its real action endpoint (which enforces the fds_sys > 4 gate itself); this module only
|
|
renders and reports. Add a card per action in `cards`. */
|
|
let $spc = {
|
|
init2: function () {
|
|
$ocms.getScript([], function () { $spc.init3(); });
|
|
},
|
|
init3: function () {
|
|
$fis.cf(true);
|
|
$fis.lf(true);
|
|
$('#activemodule').text($spt.mdl);
|
|
$('#topbar').ocmsmenu([]);
|
|
let frm = $$.dc('spcfrm', $('#contentframe'));
|
|
$$.dc('spc_intro', frm).text($spt.intro);
|
|
let grid = $$.dc('spc_grid', frm);
|
|
$.each($spc.cards, function (i, build) { build(grid); });
|
|
},
|
|
|
|
/* ── building blocks ─────────────────────────────────────────────────── */
|
|
card: function (grid, title, desc) {
|
|
let card = $$.dc('spc_card', grid);
|
|
let hd = $$.dc('spc_card_hd', card);
|
|
$$.s(title).appendTo(hd);
|
|
let bd = $$.dc('spc_card_bd', card);
|
|
if (desc) { $$.dc('spc_card_desc', bd).text(desc); }
|
|
let form = $('<form/>', { class: 'spc_form', novalidate: 'novalidate' }).appendTo(bd);
|
|
let result = $$.dc('spc_result', bd).hide();
|
|
return { card: card, body: bd, form: form, result: result };
|
|
},
|
|
field: function (form, name, label, options) {
|
|
options = options || {};
|
|
let fg = $$.dc('spc_field', form);
|
|
let id = 'spc_' + name;
|
|
$$.lbl(label, { for: id }).appendTo(fg);
|
|
let inp = $$.i({ id: id, name: name, type: options.type || 'text', placeholder: options.placeholder || '' }).appendTo(fg);
|
|
if (options.required) { inp.attr('required', 'required'); }
|
|
if (options.pattern) { inp.attr('pattern', options.pattern); }
|
|
if (options.note) { $$.dc('spc_field_note', fg).text(options.note); }
|
|
return inp;
|
|
},
|
|
showResult: function (result, ok, lines) {
|
|
result.empty().attr('class', 'spc_result ' + (ok ? 'spc_result_ok' : 'spc_result_err'));
|
|
$.each([].concat(lines || []), function (i, l) { if (l) { $$.dc('spc_result_line', result).text(l); } });
|
|
result.show();
|
|
},
|
|
/* Wires a card form: validates, confirms, POSTs, reports. `run(values)` must return a
|
|
postXT options object without success/error/complete; `report(response)` returns
|
|
{ ok, lines }. Callbacks run inside jQuery's dispatch and must never throw. */
|
|
submit: function (c, options) {
|
|
let btn = $$.bbtn(options.button, 'spc_btn spc_btn_primary').attr('type', 'submit').appendTo($$.dc('spc_actions', c.form));
|
|
c.form.on('submit', function (ev) {
|
|
ev.preventDefault();
|
|
let f = c.form[0];
|
|
if (typeof f.reportValidity === 'function' && !f.reportValidity()) { return; }
|
|
let values = c.form.serializeObject(true, { typedvalues: false });
|
|
if (values === null) { c.form.rejectInvalid(); return; }
|
|
if (typeof options.confirm === 'function' && !confirm(options.confirm(values))) { return; }
|
|
c.result.hide();
|
|
btn.prop('disabled', true).text($spt.working);
|
|
$ocms.postXT($.extend(options.run(values), {
|
|
success: function (r) {
|
|
let rep = options.report(r || {});
|
|
$spc.showResult(c.result, rep.ok, rep.lines);
|
|
},
|
|
error: function (jqXHR) {
|
|
$spc.showResult(c.result, false, [$spt.failed, (jqXHR && jqXHR.internalText) || '']);
|
|
},
|
|
complete: function () { btn.prop('disabled', false).text(options.button); }
|
|
}));
|
|
});
|
|
},
|
|
|
|
/* ── cards ───────────────────────────────────────────────────────────── */
|
|
cards: [
|
|
/* Invoice file backup + clear → POST /do/admin/backupclearinvoicefile/<id> (ADR 0027). */
|
|
function (grid) {
|
|
let c = $spc.card(grid, $spt.ifc_title, $spt.ifc_desc);
|
|
$spc.field(c.form, 'id', $spt.ifc_id, { required: true, placeholder: $spt.ifc_id_ph, note: $spt.ifc_id_note, pattern: '[A-Za-z0-9]{1,10}' });
|
|
$spc.submit(c, {
|
|
button: $spt.ifc_btn,
|
|
confirm: function (v) { return $spt.ifc_confirm.replace('{0}', v.id); },
|
|
run: function (v) {
|
|
return { url: $ocms.url('admin/backupclearinvoicefile/' + encodeURIComponent((v.id || '').trim())), method: 'post', data: {} };
|
|
},
|
|
report: function (r) {
|
|
let b = r.backup || {};
|
|
if (b.success === true) {
|
|
return { ok: true, lines: [$spt.ifc_ok, b.blobBackupUri ? $spt.ifc_ok_blob.replace('{0}', b.blobBackupUri) : $spt.ifc_ok_noblob] };
|
|
}
|
|
return { ok: false, lines: [$spt.ifc_reason[b.reason] || ($spt.failed + ' (' + (b.reason || '?') + ')')] };
|
|
}
|
|
});
|
|
}
|
|
]
|
|
};
|
|
let $$spc = { init2: $spc.init2, auth: {} };
|
|
export default $$spc;
|