Files
Fuchs_Intranet/Fuchs/js/intranet/fis_main.js
T
StefanandClaude Opus 5.5 4443bba63c
Playwright Tests / test (push) Canceled after 0s
fix(forms): never submit an invalid form; unlock password-change dialog
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>
2026-10-06 18:11:05 +02:00

372 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
$ocms.init = function (ev) {
var mdl = typeof ev === 'string' ? ev : ((ev.data || {}).fn || '');
if (mdl === '') {
return;
} else if (mdl === 'home') {
$cfr(); $lfr();
$('#topbar').ocmsmenu([], true);
$('#activemodule').text($t.ov);
$fis.ov();
} else {
$cfr(); $lfr();
$('#topbar').ocmsmenu([]);
$ocms.postXT({
url: $ocms.url(mdl + '/auth'), success: function (auth) {
if (typeof $ocms[mdl] === 'undefined') {
$ocms[mdl] = {};
}
$ocms[mdl].auth = auth;
if (auth.manage > 0) {
/* ?v=<assetversion> (newest bundle write time, emitted by _Layout) busts the browser
cache of the dynamically imported module bundle + css after a gulp rebuild. */
let av = '?v=' + encodeURIComponent($ocms.auth.assetversion || '0');
$ocms.getScript({
module: mdl, script: ['/web/fis', mdl, $ocms.auth.locale || 'de', 'js'].join('.') + av, css: ['/web/fis', mdl, 'css'].join('.') + av, condition: typeof $ocms[mdl].init2 !== 'function'
}, function () { $ocms[mdl].init2(); });
}
}, error: function () {
$('#contentframe').empty();
}
});
}
};
var $fis = { auth: {} };
$fis.db = function () {
$('#mainmenu_activemodule').text($t.ov);
let cf = $(this).empty(), ovf = $$.d({ id: 'dashboard_frame' }).appendTo(cf);
$ocms.postXT({
url: $ocms.url('wdg/my'), success: function (wx) {
$.each(wx, function (ii, wi) {
var wf = $$.dc('wdg_frame', ovf, { 'data-wdg': wi }).hide();
$ocms.wdg.call(wf, { wdg: wi });
});
}, loading: ovf
});
};
$fis.ValidateEmail = function (mail) {
if (/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(mail)) {
return true;
} else {
return false;
}
};
$fis.cf = (reset) => {
let cf = $('#contentframe'); if (bool(reset, false) === true) { cf.empty().rC('hd'); } return cf;
}
$fis.lf = (reset) => {
let lf = $('#listframe'); if (bool(reset, false) === true) { lf.empty().aC('hd').rC('fix'); } return lf;
};
$fis.frm_edit = function (resethd) {
let cf = $fis.cf(false), cf2 = cf.children('.cfrm'), edf = cf.children('.edit_frm');
if (cf2.length < 1) {
cf2 = $$.dc('cfrm hd').prependTo(cf);
} else if (bool(resethd, false) === true) {
cf2.empty();
}
if (edf.length < 1) {
edf = $$.dc('edit_frm').insertAfter(cf2); //must be before list
}
return edf.empty();
};
$fis.frm_list = function (resethd, remedit) {
let cf = $fis.cf(false), cf2 = cf.children('.cfrm'), lf = cf.children('.list_frm');
if (cf2.length < 1) {
cf2 = $$.dc('cfrm hd').prependTo(cf);
} else if (bool(resethd, false) === true) {
cf2.empty();
}
if (bool(remedit, false) === true) {
cf.children('.edit_frm').remove();
}
if (lf.length < 1) {
lf = $$.dc('list_frm').appendTo(cf); //must be after list
}
return lf.empty();
};
$fis.lfm = () => {
let lf = $fis.lf(false) , h = lf.children('.lfrm');
if (h.length < 1) {
h = $$.dc('lfrm').prependTo(lf);
}
return h;
};
$fis.getAuth = (module, force) => {
return new Promise((resolve, reject) => {
if ($fis.auth[module] && bool(force, false) === false) {
resolve($fis.auth[module] || -1);
} else {
$ocms.postXT({
url: $ocms.url('auth'), data: { module: module }, success: (response) => {
$fis.auth[module] = response.auth || -1;
resolve($fis.auth[module] || -1);
}, error: () => {
reject();
}
});
}
});
};
$fis.prepAuth = (modules) => {
return new Promise((resolve, reject) => {
$ocms.postXT({
url: $ocms.url('auth'), data: { module: modules, array: 1 }, success: (response) => {
$.extend($fis.auth, response || {});
}, complete: () => {
resolve();
}
});
});
};
$fis.isAuth = (module, min) => {
return ($fis.auth[module] || -1) >= (min || 1);
};
$fis.resetPass = function (id, fds) {
if (confirm($t.smsc)) {
$ocms.postXT({ url: $ocms.url('account/sms'), data: { fn: 'pwc'} });
$ocms.dlgform($fd.rsp.clone(), {
title: $t.rsp || '',
submit: function (e) {
var c = $(this).ldng(1);
var values = c.serializeObject(true, { typedvalues: true });
if (values === null) { c.rejectInvalid(); return; }
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, values);
if ((qs.npw || '') !== (qs.npwc || '')) {
/* The browser blocks every later submit while npw carries a custom error, so it
must be cleared as soon as either password field is edited again. */
var npw = c.find('input[name="npw"]:first');
npw[0].setCustomValidity($t.pnm);
c.find('input[name="npw"], input[name="npwc"]').off('input.pnm').on('input.pnm', function () {
npw[0].setCustomValidity('');
});
c.rejectInvalid();
} else {
$ocms.postXT({
url: $ocms.url('account/changepassword'), data: qs, success: function (response) {
alert($t.cps);
c.trigger('modal_close');
}, error: function (x) {
alert($t.rspf[x.getResponseHeader('x-ocms-std')]);
}, complete: function () {
c.ldng(0);
}, timeout: 60000
});
}
}
});
}
};
$fis.wdg = function (options) {
let wf = $(this).empty();
$ocms.postXT({
url: $ocms.url('wdg/one'), data: { short_name: options.wdg }, timeout: 90000, success: function (response, textStatus, jqXHR) {
let wi = options.wdg, wx = response[wi];
if (!wx) { wf.ldng(0); return; }
let dbl = $.inArrayRegEx('dblwidth', wx.rendering_options) > -1, tiny = $.inArrayRegEx('tiny', wx.rendering_options) > -1;
wf.toggleClass('dbl', dbl && !tiny).toggleClass('tny', tiny);
let whd = $$.dc('wdg_hd', wf, { title: ne(wx.description, $t.wdc) }).toggleClass('dbl', dbl).text(ne(wx.name, options.wdg)).dblclick(function (ev) {
ev.stopPropagation();
$fis.wdg.call(wf, { wdg: wi });
});
let wct = $$.dc('wdg_cnt', wf).toggleClass('dbl', dbl).hide();
let bgcolix = $.inArrayRegEx('bgcolor', wx.rendering_options);
if (bgcolix > -1) { wct.css('backgroundColor', wx.rendering_options[bgcolix].toString().right(':')); }
switch (wx.type) {
case 'table':
var tblset = $$.tblset({}, wct);
var thr = $$.tr().appendTo(tblset.hd);
var $cc = $t.wdg[wi.indexOf('wdg_ev_') >= 0 ? 'wdg_ev_' : wi] || {};
$.each(wx.columns, function (ci, col) {
var cc = (!$cc[col]) ? col : $cc[col].label;
var thc = $$.th().text(cc).appendTo(thr);
});
$.each(wx.data, function (di, dx) {
var tdr = $$.tr().appendTo(tblset.bdy);
$.each(wx.columns, function (ci, col) {
var tdc = $$.td().appendTo(tdr);
if (dx[col] instanceof Date || $ocms.isJSONDateString(dx[col]) === true) {
tdc.text(fdt(dx[col], $t.dateformat));
} else {
tdc.rwText(dx[col]);
}
});
//if (dx.person_guid || false) { tdr.addClass('clickable').dblclick($.proxy($fis.i_vmm, tdr, dx.person_guid[1])); }
});
if ($.inArray('firstrow_bold', wx.rendering_options) > -1) { thr.nextAll('tr:first').css('font-weight', 'bold'); }
break;
case 'ind':
$$.dc('ind', wct).addClass('sts_' + (wx.data.status || '')).append([$$.dc('ind').text(wx.data.value), $$.lbl(wx.data.label)]);
break;
case 'image_url':
wct.css('background', 'url(\'' + wx.url + '\') no-repeat center center transparent');
break;
case 'image_base64':
wct.css('background', 'url(\'data:image/png;base64,' + wx.image + '\') no-repeat center center transparent');
break;
case 'html':
wct.html(wx.html);
if ($.inArray('reload_10min', wx.rendering_options) > -1) {
var ifr = wct.find('iframe');
setTimeout(function () { ifr.attr('src', function (i, val) { return val; }); }, (10 * 60 * 1000));
}
break;
default:
}
if ($.inArray('reload_30min', wx.rendering_options) > -1 && wx.type !== 'html') {
setTimeout(function () { $fis.wdg.call(wf, { wdg: wi }); }, (30 * 60 * 1000));
}
if (wf.is(':visible')) {
wct.slideDown(150);
} else {
wct.show();
wf.fadeIn(150);
}
}, error: function (jqXHR) {
if (wf.is(':visible')) { wf.slideUp(150); }
$fis.failure.call(this, jqXHR);
}, complete: function () {
wf.ldng(0).trigger('wdg:settled');
}
});
};
$fis.ov = function () {
//$('#mainmenu_activemodule').text("");
$fis.lf(true);
let cf = $('#contentframe').empty(), ovf = $$.d({ id: 'dashboard_frame' }).appendTo(cf);
$ocms.postXT({
url: $ocms.url('wdg/my'), success: function (wx) {
let ind = $$.dc('wdg_loading_ind', ovf).text($t.wldg || '').hide();
let pending = wx.length;
let settle = function () {
if (--pending <= 0) { ind.fadeOut(150); }
};
if (pending > 0) { ind.show(); }
$.each(wx, function (ii, wi) {
var wf = $$.dc('wdg_frame', ovf, { 'data-wdg': wi }).hide().one('wdg:settled', settle);
$fis.wdg.call(wf, { wdg: wi });
});
}, loading: ovf
});
};
$fis.notifications = {
connection: null,
init: function () {
if (typeof signalR === 'undefined' || this.connection !== null || !$ocms.auth.useraccount_id) {
return;
}
this.ensureFrame();
this.connection = new signalR.HubConnectionBuilder()
.withUrl('/notifications')
.withAutomaticReconnect()
.build();
this.connection.on('notification', (notification) => {
this.push(notification);
});
// withAutomaticReconnect() only retries a connection that was successfully
// established and later dropped — it does not retry a failed initial start()
// (e.g. the server restarting mid-debug). Without this, a single failed start()
// silently blackholes every notification for the rest of the page's lifetime.
this.connection.onclose(() => {
console.warn('Notification connection closed; retrying in 5s.');
this.connection = null;
setTimeout(() => this.init(), 5000);
});
this.start();
},
start: function () {
this.connection.start().catch((err) => {
console.warn('Notification connection failed to start; retrying in 5s.', err);
this.connection = null;
setTimeout(() => this.init(), 5000);
});
},
ensureFrame: function () {
if ($('#notification_frame').length < 1) {
$('<div/>', { id: 'notification_frame' }).appendTo($('footer:first').length ? 'footer:first' : 'body');
}
},
push: function (notification) {
this.ensureFrame();
notification = notification || {};
let item = $('<div/>', { class: 'notification_item' })
.addClass((notification.severity || 'info').toLowerCase())
.append($('<button/>', { type: 'button', class: 'notification_close', text: '×' }))
.append($('<div/>', { class: 'notification_title', text: notification.title || 'Info' }))
.append($('<div/>', { class: 'notification_message', text: notification.message || '' }));
item.find('.notification_close').on('click', function () {
item.remove();
});
$('#notification_frame').prepend(item);
setTimeout(function () {
item.fadeOut(150, function () { item.remove(); });
}, 9000);
}
};
/* Live draft-editing client (ADR 0006/0007). Separate SignalR connection to the
dedicated /draftpreview hub; the server signals the *one* browser editing a draft
(group = session token) to re-fetch (draftReady), warns before idle expiry
(draftExpiring), and tells it to close on eviction (draftClosed). The editor
(fis.inv_shared.js) registers the open draft via $fis.draft.bind(token, {...}). */
$fis.draft = {
connection: null,
active: null, /* { token, onReady(version), onExpiring(secondsLeft), onClosed(reason) } */
init: function () {
if (typeof signalR === 'undefined' || this.connection !== null || !$ocms.auth.useraccount_id) {
return;
}
this.connection = new signalR.HubConnectionBuilder()
.withUrl('/draftpreview')
.withAutomaticReconnect()
.build();
this.connection.on('draftReady', (p) => this._dispatch('onReady', p, (p) => p.version));
this.connection.on('draftExpiring', (p) => this._dispatch('onExpiring', p, (p) => p.secondsLeft));
this.connection.on('draftClosed', (p) => this._dispatch('onClosed', p, (p) => p.reason));
/* Re-join the active draft's group after a (re)connect — group membership is
per-connection and is lost when the socket drops. */
this.connection.onreconnected(() => { if (this.active) { this._invoke('JoinDraft', this.active.token); } });
this.connection.onclose(() => {
console.warn('Draft connection closed; retrying in 5s.');
this.connection = null;
setTimeout(() => { this.init(); if (this.active) { this.bind(this.active.token, this.active); } }, 5000);
});
this.start();
},
start: function () {
this.connection.start()
.then(() => { if (this.active) { this._invoke('JoinDraft', this.active.token); } })
.catch((err) => {
console.warn('Draft connection failed to start; retrying in 5s.', err);
this.connection = null;
setTimeout(() => this.init(), 5000);
});
},
/* Registers the currently open draft and joins its signal group. handlers:
{ onReady, onExpiring, onClosed }. */
bind: function (token, handlers) {
if (!token) { return; }
this.active = $.extend({ token: token }, handlers || {});
if (this.connection === null) { this.init(); }
this._invoke('JoinDraft', token);
},
/* Unregisters + leaves the group (editor closed). */
release: function (token) {
if (this.active && (!token || this.active.token === token)) { this.active = null; }
this._invoke('LeaveDraft', token);
},
_invoke: function (method, token) {
if (!token || !this.connection || this.connection.state !== 'Connected') { return; }
this.connection.invoke(method, token).catch((err) => console.warn('Draft ' + method + ' failed', err));
},
_dispatch: function (handler, payload, argOf) {
payload = payload || {};
if (!this.active || this.active.token !== payload.token) { return; }
if (typeof this.active[handler] === 'function') { this.active[handler](argOf(payload)); }
}
};