javascript and api to stop and display active records (#772)

This commit is contained in:
Kevin Papst
2019-05-10 13:45:09 +02:00
committed by GitHub
parent e619b5fd31
commit 09da7cd242
86 changed files with 918 additions and 449 deletions

View File

@@ -1,15 +1,14 @@
// ------------------- INLINED ADMIN-LTE DEFINITIONS -------------------
// this was done to save around 300kb by:
// require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte');
// this was replaced to save around 300kb by:
// - removing moment locales which are not used
// - removing fullcalendar locales which are not used
// - removing icheck which is not used
//require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte');
// - removing jquery-ui which is not used?
const $ = require('jquery');
global.$ = global.jQuery = $;
require('jquery-ui');
require('bootstrap-sass');
require('jquery-slimscroll');
require('bootstrap-select');

View File

@@ -25,8 +25,11 @@ import KimaiSelectDataAPI from "./plugins/KimaiSelectDataAPI";
import KimaiDateTimePicker from "./plugins/KimaiDateTimePicker";
import KimaiAlternativeLinks from "./plugins/KimaiAlternativeLinks";
import KimaiAjaxModalForm from "./plugins/KimaiAjaxModalForm";
import KimaiActiveRecords from "./plugins/KimaiActiveRecords";
import KimaiRecentActivities from "./plugins/KimaiRecentActivities";
import KimaiEvent from "./plugins/KimaiEvent";
import KimaiAPILink from "./plugins/KimaiAPILink";
import KimaiAlert from "./plugins/KimaiAlert";
export default class KimaiLoader {
@@ -64,19 +67,22 @@ export default class KimaiLoader {
kimai.registerPlugin(new KimaiEvent());
kimai.registerPlugin(new KimaiAPI());
kimai.registerPlugin(new KimaiAlert());
kimai.registerPlugin(new KimaiActiveRecordsDuration('[data-since]'));
kimai.registerPlugin(new KimaiDatatableColumnView('data-column-visibility'));
kimai.registerPlugin(new KimaiThemeInitializer());
kimai.registerPlugin(new KimaiJqueryPluginInitializer());
kimai.registerPlugin(new KimaiDateRangePicker('.content-wrapper'));
kimai.registerPlugin(new KimaiDateTimePicker('.content-wrapper'));
kimai.registerPlugin(new KimaiDatatable());
kimai.registerPlugin(new KimaiDatatable('table.dataTable'));
kimai.registerPlugin(new KimaiToolbar());
kimai.registerPlugin(new KimaiSelectDataAPI('select[data-related-select]'));
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
//kimai.registerPlugin(new KimaiPauseRecord('li.messages-menu ul.menu li'));
kimai.registerPlugin(new KimaiRecentActivities('li.notifications-menu'));
kimai.registerPlugin(new KimaiActiveRecords('li.messages-menu', 'li.messages-menu-empty'));
kimai.registerPlugin(new KimaiAPILink('a.api-link'));
//kimai.registerPlugin(new KimaiPauseRecord('li.messages-menu ul.menu li'));
// notify all listeners that Kimai plugins can now be registered
kimai.getPlugin('event').trigger('kimai.pluginRegister');

View File

@@ -6,7 +6,7 @@
*/
/*!
* [KIMAI] KimaiToolbar: some event listener to handle the toolbar/data-table filter, toolbar and navigation
* [KIMAI] KimaiAPI: easy access to API methods
*/
import jQuery from 'jquery';
@@ -31,4 +31,18 @@ export default class KimaiAPI extends KimaiPlugin {
});
}
patch(url, callbackSuccess, callbackError) {
jQuery.ajax({
url: url,
headers: {
'X-AUTH-SESSION': true,
'Content-Type':'application/json'
},
method: 'PATCH',
dataType: 'json',
success: callbackSuccess,
error: callbackError
});
}
}

View File

@@ -0,0 +1,70 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiAPILink
*
* allows to assign the given selector to any element, which then is used as click-handler
* calling an API method and trigger the event from data-event attribute afterwards
*/
import KimaiClickHandlerReducedInTableRow from "./KimaiClickHandlerReducedInTableRow";
export default class KimaiAPILink extends KimaiClickHandlerReducedInTableRow {
constructor(selector) {
super();
this.selector = selector;
}
init() {
const self = this;
document.addEventListener('click', function(event) {
let target = event.target;
while (!target.matches('body')) {
if (target.matches(self.selector)) {
const attributes = target.dataset;
let url = attributes['href'];
if (!url) {
url = target.getAttribute('href');
}
const method = attributes['method'];
const eventName = attributes['event'];
const api = self.getContainer().getPlugin('api');
const eventing = self.getContainer().getPlugin('event');
const alert = self.getContainer().getPlugin('alert');
if (method === 'PATCH') {
api.patch(url, function(result) {
eventing.trigger(eventName);
if (attributes.msgSuccess) {
alert.success(attributes.msgSuccess);
}
}, function(xhr, err) {
let message = 'action.update.error';
if (attributes.msgError) {
message = attributes.msgError;
}
if (xhr.responseJSON && xhr.responseJSON.message) {
err = xhr.responseJSON.message;
}
alert.error(message, err);
});
}
event.preventDefault();
event.stopPropagation();
}
target = target.parentNode;
}
});
}
}

View File

@@ -0,0 +1,113 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiActiveRecords: responsible to display the users active records
*/
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiActiveRecords extends KimaiPlugin {
constructor(selector, selectorEmpty) {
super();
this.selector = selector;
this.selectorEmpty = selectorEmpty;
}
getId() {
return 'active-records';
}
init() {
const menu = document.querySelector(this.selector);
// the menu can be hidden if user has no permissions to see it
if (menu === null) {
return;
}
const dropdown = menu.querySelector('ul.dropdown-menu');
this.attributes = dropdown.dataset;
this.itemList = dropdown.querySelector('li > ul.menu');
this.label = menu.querySelector('a > span.label');
const self = this;
const handle = function() { self.reloadActiveRecords(); };
document.addEventListener('kimai.timesheetUpdate', handle);
document.addEventListener('kimai.activityUpdate', handle);
document.addEventListener('kimai.projectUpdate', handle);
document.addEventListener('kimai.customerUpdate', handle);
}
emptyList() {
this.itemList.innerHTML = '';
}
_toggleMenu(hasEntries) {
const menu = document.querySelector(this.selector);
const menuEmpty = document.querySelector(this.selectorEmpty);
menu.style.display = hasEntries ? 'inline-block' : 'none';
if (menuEmpty !== null) {
menuEmpty.style.display = !hasEntries ? 'inline-block' : 'none';
}
}
setEntries(entries) {
this._toggleMenu(entries.length > 0);
if (entries.length === 0) {
this.label.innerText = '';
this.emptyList();
return;
}
let htmlToInsert = '';
const durations = this.getContainer().getPlugin('timesheet-duration');
for (let timesheet of entries) {
htmlToInsert +=
`<li>` +
`<a href="${ this.attributes['href'].replace('000', timesheet.id) }" data-event="kimai.timesheetStop kimai.timesheetUpdate" class="api-link" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">` +
`<div class="pull-left">` +
`<i class="${ this.attributes['icon'] } fa-2x"></i>` +
`</div>` +
`<h4>` +
`<span>${ timesheet.activity.name }</span>` +
`<small>` +
`<span data-title="true" data-since="${ timesheet.begin }" data-format="${ this.attributes['format'] }">${ durations.formatDuration(timesheet.duration, this.attributes['format']) }</span>` +
`</small>` +
`</h4>` +
`<p>${ timesheet.project.name } (${ timesheet.project.customer.name })</p>` +
`</a>` +
`</li>`;
}
if (this.label.dataset.warning < entries.length) {
this.label.classList = 'label label-danger';
} else {
this.label.classList = 'label label-warning';
}
this.label.innerText = entries.length;
this.itemList.innerHTML = htmlToInsert;
durations.updateRecords();
}
reloadActiveRecords() {
const self = this;
const apiService = this.getContainer().getPlugin('api');
apiService.get(this.attributes['api'], function(result) {
self.setEntries(result);
});
}
}

View File

@@ -19,19 +19,17 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin {
this.selector = selector;
}
init() {
this.updateRecords();
this.registerUpdates(10000);
getId() {
return 'timesheet-duration';
}
registerUpdates(interval) {
let self = this;
this._updatesHandler = setInterval(
function() {
self.updateRecords();
},
interval
);
init() {
this.updateRecords();
const self = this;
const handle = function() { self.updateRecords(); };
this._updatesHandler = setInterval(handle, 10000);
// this will probably not work as expected, as other event-handler might need longer to update the DOM
document.addEventListener('kimai.timesheetUpdate', handle);
}
unregisterUpdates() {
@@ -40,10 +38,17 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin {
updateRecords() {
let durations = [];
for(let record of document.querySelectorAll(this.selector)) {
const activeRecords = document.querySelectorAll(this.selector);
if (activeRecords.length === 0) {
document.title = document.querySelector('body').dataset['title'];
return;
}
for(let record of activeRecords) {
const since = record.getAttribute('data-since');
const format = record.getAttribute('data-format');
const duration = KimaiActiveRecordsDuration._getDuration(since, format);
const duration = this.formatDuration(since, format);
if (record.getAttribute('data-title') !== null) {
durations.push(duration);
}
@@ -51,7 +56,7 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin {
}
if (durations.length === 0) {
return this;
return;
}
let title = durations.shift();
@@ -61,11 +66,9 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin {
title += prefix + duration;
}
document.title = title;
return this;
}
static _getDuration(since, format) {
formatDuration(since, format) {
const duration = moment.duration(moment(new Date()).diff(moment(since)));
let hours = parseInt(duration.asHours()).toString();

View File

@@ -67,8 +67,13 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
jQuery(html).find('#form_modal .modal-content')
);
// TODO these should be handled with Events, probably using Custom Events
// https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
// activate new loaded widgets
self.getContainer().getPlugin('date-time-picker').activateDateTimePicker(formIdentifier);
// activate selectpicker if beta test is active
jQuery('.selectpicker').selectpicker('refresh');
}
// show error flash messages
@@ -100,9 +105,10 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
// click handler for modal save button, to send forms via ajax
form.on('submit', function(event){
let btn = jQuery(formIdentifier + ' button[type=submit]').button('loading');
let eventName = form.attr('data-form-event');
let events = self.getContainer().getPlugin('event');
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');
event.preventDefault();
event.stopPropagation();
@@ -121,16 +127,40 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
self._openFormInModal(html);
} else {
events.trigger(eventName);
self.getContainer().getPlugin('datatable').reload();
// 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';
}
remoteModal.modal('hide');
alert.success(msg);
}
return false;
},
error: function(xhr, err) {
// FIXME problem in google and 500 error, keeps on submitting...
// what else could we do? submitting again at least gives us the opportunity to see errors,
// which maybe would be hidden otherwise... this one is totally up for discussion!
form.submit();
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);
}
});
});

View File

@@ -0,0 +1,54 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiAlert: notifications for Kimai
*/
import Swal from 'sweetalert2'
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiAlert extends KimaiPlugin {
getId() {
return 'alert';
}
error(title, message) {
const translation = this.getContainer().getTranslation();
if (translation.has(title)) {
title = translation.get(title);
}
if (translation.has(message)) {
message = translation.get(message);
}
Swal.fire({
type: 'error',
title: title.replace('%reason%', ''),
text: message,
});
}
success(message) {
const translation = this.getContainer().getTranslation();
if (translation.has(message)) {
message = translation.get(message);
}
Swal.fire({
//toast: true,
//timer: 3000,
timer: 1500,
position: 'top-end',
showConfirmButton: false,
type: 'success',
title: message,
});
}
}

View File

@@ -14,12 +14,42 @@ import KimaiPlugin from "../KimaiPlugin";
export default class KimaiDatatable extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'datatable';
}
reload() {
let form = jQuery('.toolbar form');
init() {
const dataTable = document.querySelector(this.selector);
// not every page contains a dataTable
if (dataTable === null) {
return;
}
const attributes = dataTable.dataset;
const events = attributes['reloadEvent'];
if (events === undefined) {
return;
}
const self = this;
const handle = function() { self.reloadDatatable(); };
for (let eventName of events.split(' ')) {
document.addEventListener(eventName, handle);
}
}
reloadDatatable() {
// FIXME remove query
const durations = this.getContainer().getPlugin('timesheet-duration');
const form = jQuery('.toolbar form');
let loading = '<div class="overlay"><i class="fas fa-sync fa-spin"></i></div>';
jQuery('section.content').append(loading);
@@ -38,6 +68,7 @@ export default class KimaiDatatable extends KimaiPlugin {
jQuery('section.content').replaceWith(
jQuery(html).find('section.content')
);
durations.updateRecords();
},
error: function(xhr, err) {
form.submit();

View File

@@ -18,11 +18,13 @@ export default class KimaiEvent extends KimaiPlugin {
}
trigger(name) {
if (name === null) {
if (name === null || name === undefined) {
return;
}
document.dispatchEvent(new Event(name));
for(let event of name.split(' ')) {
document.dispatchEvent(new Event(event));
}
}
}

View File

@@ -35,11 +35,12 @@ export default class KimaiRecentActivities extends KimaiPlugin {
this.itemList = dropdown.querySelector('li > ul.menu');
const self = this;
const handle = function() { self.reload(); };
const handle = function() { self.reloadRecentActivities(); };
// don't block initial browser rendering
setTimeout(handle, 500);
document.addEventListener('kimai.timesheetStop', handle);
document.addEventListener('kimai.activityUpdate', handle);
document.addEventListener('kimai.projectUpdate', handle);
document.addEventListener('kimai.customerUpdate', handle);
@@ -63,13 +64,13 @@ export default class KimaiRecentActivities extends KimaiPlugin {
.replace('%project%', timesheet.project.name)
.replace('%activity%', timesheet.activity.name);
htmlToInsert += `<li><a href="${ this.attributes['url'].replace('000', timesheet.id) }"><i class="${ this.attributes['icon'] }"></i> ${ label }</a></li>`;
htmlToInsert += `<li><a href="${ this.attributes['href'].replace('000', timesheet.id) }"><i class="${ this.attributes['icon'] }"></i> ${ label }</a></li>`;
}
this.itemList.innerHTML = htmlToInsert;
}
reload() {
reloadRecentActivities() {
const self = this;
const apiService = this.getContainer().getPlugin('api');

View File

@@ -15,7 +15,7 @@ import KimaiPlugin from "../KimaiPlugin";
export default class KimaiToolbar extends KimaiPlugin {
init() {
const self = this;
const datatable = this.getContainer().getPlugin('datatable');
// This catches all clicks on the pagination and prevents the default action, as we want to relad the page via JS
jQuery('body').on('click', 'div.navigation ul.pagination li a', function(event) {
@@ -41,7 +41,7 @@ export default class KimaiToolbar extends KimaiPlugin {
default:
jQuery('.toolbar form input#page').val(1);
}
self._reloadDatatable();
datatable.reloadDatatable();
});
jQuery('.toolbar form select').change(function (event) {
@@ -61,13 +61,9 @@ export default class KimaiToolbar extends KimaiPlugin {
}
jQuery('.toolbar form input#page').val(1);
if (reload) {
self._reloadDatatable();
datatable.reloadDatatable();
}
});
}
_reloadDatatable() {
this.getContainer().getPlugin('datatable').reload();
}
}

View File

@@ -21,3 +21,4 @@
@import 'navbar';
@import 'daterangepicker';
@import 'skins';
@import 'sweetalert';

View File

@@ -25,9 +25,6 @@
.messages-menu>.dropdown-menu>li .menu>li>a>h4>span {
margin-right: 55px;
}
.start_record {
text-align: center;
}
.dropdown-menu {
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
max-width: 90vw;

View File

@@ -0,0 +1,3 @@
.swal2-popup {
font-size: unset;
}