Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)

This commit is contained in:
Kevin Papst
2022-12-31 21:19:55 +01:00
committed by GitHub
parent 95e06746bd
commit 90a0fd8a22
2164 changed files with 83700 additions and 86426 deletions

View File

@@ -12,70 +12,87 @@
* opening a modal with the content from the URL given in the elements 'data-href' or 'href' attribute
*/
import jQuery from 'jquery';
import KimaiReducedClickHandler from "./KimaiReducedClickHandler";
import { Modal } from 'bootstrap';
export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
constructor(selector) {
super();
this.selector = selector;
this._selector = selector;
}
getId() {
getId()
{
return 'modal';
}
init() {
const self = this;
this.isDirty = false;
init()
{
this._isDirty = false;
this.modal = jQuery('#remote_form_modal');
this.modal
.on('hide.bs.modal', function (e) {
if (self.isDirty) {
if (jQuery('#remote_form_modal .modal-body .remote_modal_is_dirty_warning').length === 0) {
const msg = self.getContainer().getTranslation().get('modal.dirty');
jQuery('#remote_form_modal .modal-body').prepend('<p class="'+(self.modal.hasClass('modal-danger') ? 'well well-sm ' : '') + 'text-danger small remote_modal_is_dirty_warning">' + msg + '</p>');
}
e.preventDefault();
return;
const modalElement = this._getModalElement();
if (modalElement === null) {
return;
}
modalElement.addEventListener('hide.bs.modal', (event) => {
if (this._isDirty) {
if (modalElement.querySelector('.modal-body .remote_modal_is_dirty_warning') === null) {
const msg = this.translate('modal.dirty');
const temp = document.createElement('div');
temp.innerHTML = '<p class="text-danger small remote_modal_is_dirty_warning">' + msg + '</p>';
modalElement.querySelector('.modal-body').prepend(temp.firstElementChild);
}
jQuery(self._getFormIdentifier()).off('change', self._isDirtyHandler);
self.isDirty = false;
self.getContainer().getPlugin('event').trigger('modal-hide');
})
.on('hidden.bs.modal', function () {
// kill all references, so GC can kick in
self.getContainer().getPlugin('form').destroyForm(self._getFormIdentifier());
jQuery('#remote_form_modal .modal-body').replaceWith('');
})
.on('show.bs.modal', function () {
self.getContainer().getPlugin('event').trigger('modal-show');
});
event.preventDefault();
return;
}
this._isDirty = false;
document.dispatchEvent(new Event('modal-hide'));
});
this._addClickHandler(this.selector, function(href) {
self.openUrlInModal(href);
modalElement.addEventListener('hidden.bs.modal', () => {
// kill all references, so GC can kick in
this.getContainer().getPlugin('form').destroyForm(this._getFormIdentifier());
modalElement.querySelector('.modal-body').replaceWith('');
});
modalElement.addEventListener('show.bs.modal', () => {
document.dispatchEvent(new Event('modal-show'));
});
this.addClickHandler(this._selector, (href) => {
this.openUrlInModal(href);
});
}
openUrlInModal(url, errorHandler) {
const self = this;
_getModal()
{
return Modal.getOrCreateInstance(this._getModalElement())
}
if (errorHandler === undefined) {
errorHandler = function(xhr, err) {
if (xhr.status === undefined || xhr.status !== 403) {
window.location = url;
}
};
}
openUrlInModal(url)
{
const headers = new Headers();
headers.append('X-Requested-With', 'Kimai-Modal');
jQuery.ajax({
url: url,
success: function(html) {
self._openFormInModal(html);
},
error: errorHandler
this.fetch(url, {
method: 'GET',
redirect: 'follow',
headers: headers
})
.then(response => {
if (!response.ok) {
window.location = url;
return;
}
return response.text().then(html => {
this._openFormInModal(html);
});
})
.catch(() => {
window.location = url;
});
}
@@ -85,157 +102,185 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
* @returns {string}
* @private
*/
_getFormIdentifier() {
_getFormIdentifier()
{
return '#remote_form_modal .modal-content form';
}
_openFormInModal(html) {
const self = this;
/**
* @returns {HTMLElement|null}
* @private
*/
_getModalElement()
{
return document.getElementById('remote_form_modal');
}
let formIdentifier = this._getFormIdentifier();
// if any of these is found in a response, the form will be re-displayed
let flashErrorIdentifier = 'div.alert-error';
// messages to show above the form
let flashMessageIdentifier = 'div.alert';
let form = jQuery(formIdentifier);
let remoteModal = this.modal;
/**
* @param {Element|ChildNode} node
* @returns {Element}
* @private
*/
_makeScriptExecutable(node) {
if (node.tagName !== undefined && node.tagName === 'SCRIPT') {
const script = document.createElement('script');
script.text = node.innerHTML;
node.parentNode.replaceChild(script, node);
} else {
for (const child of node.childNodes) {
this._makeScriptExecutable(child);
}
}
// will be (re-)activated later
form.off('submit');
return node;
}
_openFormInModal(html)
{
const formIdentifier = this._getFormIdentifier();
let remoteModal = this._getModalElement();
const newFormHtml = document.createElement('div');
newFormHtml.innerHTML = html;
const newModalContent = this._makeScriptExecutable(newFormHtml.querySelector('#form_modal .modal-content'));
// load new form from given content
if (jQuery(html).find('#form_modal .modal-content').length > 0) {
// Support changing modal importance/types
remoteModal.on('hidden.bs.modal', function () {
if (remoteModal.hasClass('modal-danger')) {
remoteModal.removeClass('modal-danger');
}
});
if (jQuery(html).find('#form_modal').hasClass('modal-danger')) {
remoteModal.addClass('modal-danger');
}
if (newModalContent !== null) {
// Support changing modal sizes
let modalDialog = remoteModal.find('.modal-dialog');
let largeModal = jQuery(html).find('.modal-dialog').hasClass('modal-lg');
if (largeModal && !modalDialog.hasClass('modal-lg')) {
modalDialog.addClass('modal-lg');
}
if (!largeModal && modalDialog.hasClass('modal-lg')) {
modalDialog.removeClass('modal-lg');
let modalDialog = remoteModal.querySelector('.modal-dialog');
let largeModal = newFormHtml.querySelector('.modal-dialog').classList.contains('modal-lg');
if (largeModal && !modalDialog.classList.contains('modal-lg')) {
modalDialog.classList.toggle('modal-lg');
}
jQuery('#remote_form_modal .modal-content').replaceWith(
jQuery(html).find('#form_modal .modal-content')
);
if (!largeModal && modalDialog.classList.contains('modal-lg')) {
modalDialog.classList.toggle('modal-lg');
}
jQuery('#remote_form_modal [data-dismiss=modal]').on('click', function() {
self.isDirty = false;
remoteModal.querySelector('.modal-content').replaceWith(newModalContent);
[].slice.call(remoteModal.querySelectorAll('[data-bs-dismiss="modal"]')).map((element) => {
element.addEventListener('click', () => {
this._isDirty = false;
this._getModal().hide();
});
});
// activate new loaded widgets
self.getContainer().getPlugin('form').activateForm(formIdentifier);
this.getContainer().getPlugin('form').activateForm(formIdentifier);
}
// show error flash messages
let flashMessages = jQuery(html).find(flashMessageIdentifier);
if (flashMessages.length > 0) {
jQuery('#remote_form_modal .modal-body').prepend(flashMessages);
let flashMessages = newFormHtml.querySelector('div.alert');
if (flashMessages !== null) {
remoteModal.querySelector('.modal-body').prepend(flashMessages);
}
// -----------------------------------------------------------------------
// a fix for firefox focus problems with datepicker in modal
// see https://github.com/kimai/kimai/issues/618
let enforceModalFocusFn = jQuery.fn.modal.Constructor.prototype.enforceFocus;
jQuery.fn.modal.Constructor.prototype.enforceFocus = function() {};
remoteModal.on('hidden.bs.modal', function () {
jQuery.fn.modal.Constructor.prototype.enforceFocus = enforceModalFocusFn;
});
// -----------------------------------------------------------------------
remoteModal.modal('show');
// the new form that was loaded via ajax
form = jQuery(formIdentifier);
const form = document.querySelector(formIdentifier);
this._isDirtyHandler = function(e) {
self.isDirty = true;
}
form.on('change', this._isDirtyHandler);
form.addEventListener('change', () => {
this._isDirty = true;
});
// click handler for modal save button, to send forms via ajax
form.on('submit', function(event) {
// if the form has a target, we let the normal HTML flow happen
if (form.attr('target') !== undefined) {
return true;
}
form.addEventListener('submit', this._getEventHandler());
// otherwise we do some AJAX magic to process the form in the background
const btn = jQuery(formIdentifier + ' button[type=submit]').button('loading');
const eventName = form.attr('data-form-event');
const events = self.getContainer().getPlugin('event');
const alert = self.getContainer().getPlugin('alert');
this._getModal().show();
}
event.preventDefault();
event.stopPropagation();
_getEventHandler()
{
if (this.eventHandler === undefined) {
this.eventHandler = (event) => {
const form = event.target;
jQuery.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: form.serialize(),
success: function(html) {
btn.button('reset');
let hasFieldError = jQuery(html).find('#form_modal .modal-content .has-error').length > 0;
let hasFormError = jQuery(html).find('#form_modal .modal-content ul.list-unstyled li.text-danger').length > 0;
let hasFlashError = jQuery(html).find(flashErrorIdentifier).length > 0;
if (hasFieldError || hasFormError || hasFlashError) {
self._openFormInModal(html);
} else {
events.trigger(eventName);
// try to find form defined messages first ...
let msg = form.attr('data-msg-success');
if (msg === null || msg === undefined) {
// ... but if none was available, check the response to find server rendered flash-message
let flashMessage = jQuery(html).find('section.content div.row div.alert.alert-success');
if (flashMessage.length > 0) {
let flashContent = flashMessage.contents();
if (flashContent.length === 3) {
msg = flashContent[2].textContent;
}
}
}
// ... and if even that is not available, we use a generic fallback message
if (msg === null || msg === undefined) {
msg = 'action.update.success';
}
self.isDirty = false;
remoteModal.modal('hide');
alert.success(msg);
}
return false;
},
error: function(xhr, err) {
let message = form.attr('data-msg-error');
if (message === null || message === undefined) {
message = 'action.update.error';
}
if (xhr.responseJSON && xhr.responseJSON.message) {
err = xhr.responseJSON.message;
} else if (xhr.status && xhr.statusText) {
err = '[' + xhr.status +'] ' + xhr.statusText;
}
alert.error(message, err);
// this is useful for changing form fields and retrying to save (and in development to test form changes)
setTimeout(function() {
btn.button('reset');
}, 1500);
// if the form has a target, we let the normal HTML flow happen
if (form.target !== undefined && form.target !== '') {
return true;
}
});
});
// otherwise we do some AJAX magic to process the form in the background
/** @type {HTMLButtonElement} btn */
const btn = document.querySelector(this._getFormIdentifier() + ' button[type=submit]');
btn.textContent = btn.textContent + ' …';
btn.disabled = true;
const eventName = form.dataset['formEvent'];
/** @type {KimaiEvent} alert */
const events = this.getContainer().getPlugin('event');
/** @type {KimaiAlert} alert */
const alert = this.getContainer().getPlugin('alert');
event.preventDefault();
event.stopPropagation();
const headers = new Headers();
headers.append('X-Requested-With', 'Kimai-Modal');
const options = {headers: headers};
this.fetchForm(form, options)
.then(response => {
response.text().then((html) => {
/** @type {HTMLDivElement} responseHtml */
const responseHtml = document.createElement('div');
responseHtml.innerHTML = html;
let hasFieldError = false;
let hasFormError = false;
let hasFlashError = false;
// button must be re-enabled anyway
btn.textContent = btn.textContent.replace(' …', '');
btn.disabled = false;
// if the request was successful, there will be no form
/** @type {Element} modalContent */
const modalContent = responseHtml.querySelector('#form_modal .modal-content');
if (modalContent !== null) {
hasFieldError = modalContent.querySelector('.is-invalid') !== null;
if (!hasFieldError) {
// happens when an error occurs for a "hidden or non-classical" form element e.g. creating team without users
hasFieldError = modalContent.querySelector('.invalid-feedback') !== null;
}
hasFormError = modalContent.querySelector('ul.list-unstyled li.text-danger') !== null;
hasFlashError = responseHtml.querySelector('div.alert-danger') !== null;
}
if (hasFieldError || hasFormError || hasFlashError) {
this._openFormInModal(html);
} else {
events.trigger(eventName);
// try to find form defined message first, but
let msg = form.dataset['msgSuccess'];
// if that is not available: use a generic fallback message
if (msg === null || msg === undefined || msg === '') {
msg = 'action.update.success';
}
this._isDirty = false;
this._getModal().hide();
alert.success(msg);
}
});
})
.catch(error => {
let message = form.dataset['msgError'];
if (message === null || message === undefined || message === '') {
message = 'action.update.error';
}
alert.error(message, error.message);
// this is useful for changing form fields and retrying to save (and in development to test form changes)
setTimeout(() =>{
// critical error, allow to re-submit?
btn.textContent = btn.textContent.replace(' …', '');
btn.disabled = false;
}, 1500);
});
};
}
return this.eventHandler;
}
}