Release 2.0.2 (#3856)
* allow to overwrite global spreadsheet styles * bump version * fix deprecations in vcard download * bump composer packages * added help page for all registered locales * fix menu id's, cleanup times route, fix configurable homepage redirect * format duration without leading zero in hours (unify javascript with php behavior) * fix active records in all screen sizes * improved responsiveness in XS * fixed invoice number for customer null fields * fix javascript respects multiple recent-activity dropdowns * show recent activities on small screens * fix user-profile layout column in XS * fix search is always marked as active
This commit is contained in:
@@ -83,7 +83,7 @@ export default class KimaiLoader {
|
||||
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
|
||||
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
|
||||
kimai.registerPlugin(new KimaiRecentActivities());
|
||||
kimai.registerPlugin(new KimaiActiveRecords('header .messages-menu', 'header .messages-menu-empty'));
|
||||
kimai.registerPlugin(new KimaiActiveRecords());
|
||||
kimai.registerPlugin(new KimaiAPILink('api-link'));
|
||||
kimai.registerPlugin(new KimaiMultiUpdateTable());
|
||||
kimai.registerPlugin(new KimaiThemeInitializer());
|
||||
|
||||
@@ -13,26 +13,28 @@ import KimaiPlugin from '../KimaiPlugin';
|
||||
|
||||
export default class KimaiActiveRecords extends KimaiPlugin {
|
||||
|
||||
constructor(selector, selectorEmpty) {
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this._selector = selector;
|
||||
this._selectorEmpty = selectorEmpty;
|
||||
this._selector = '.ticktac-menu';
|
||||
this._selectorEmpty = '.ticktac-menu-empty';
|
||||
}
|
||||
|
||||
getId() {
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getId()
|
||||
{
|
||||
return 'active-records';
|
||||
}
|
||||
|
||||
init() {
|
||||
this._menu = document.querySelector(this._selector);
|
||||
|
||||
init()
|
||||
{
|
||||
// the menu can be hidden if user has no permissions to see it
|
||||
if (this._menu === null) {
|
||||
if (document.querySelector(this._selector) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.attributes = this._menu.dataset;
|
||||
|
||||
const handleUpdate = () => {
|
||||
this.reloadActiveRecords();
|
||||
};
|
||||
@@ -49,7 +51,9 @@ export default class KimaiActiveRecords extends KimaiPlugin {
|
||||
// -----------------------------------------------------------------------
|
||||
// handle duration in the visible UI
|
||||
this._updateBrowserTitle = !!this.getConfiguration('updateBrowserTitle');
|
||||
this._updateDuration();
|
||||
|
||||
// deactivated direct duration update, becuase it is unclear
|
||||
// this._updateDuration();
|
||||
const handle = () => {
|
||||
this._updateDuration();
|
||||
};
|
||||
@@ -63,8 +67,15 @@ export default class KimaiActiveRecords extends KimaiPlugin {
|
||||
// clearInterval(this._updatesHandler);
|
||||
// }
|
||||
|
||||
_updateDuration() {
|
||||
const activeRecords = this._menu.querySelectorAll('[data-since]:not([data-since=""])');
|
||||
/**
|
||||
* Updates the duration of all running entries, both in the ticktac menus and in the listing pages.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_updateDuration()
|
||||
{
|
||||
// needs to search in document, to find all running entries, both in "ticktac" and listing pages
|
||||
const activeRecords = document.querySelectorAll('[data-since]:not([data-since=""])');
|
||||
|
||||
if (activeRecords.length === 0) {
|
||||
if (this._updateBrowserTitle) {
|
||||
@@ -94,57 +105,70 @@ export default class KimaiActiveRecords extends KimaiPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._updateBrowserTitle) {
|
||||
return;
|
||||
if (this._updateBrowserTitle) {
|
||||
// only show the first found record, even if we have more
|
||||
document.title = durations.shift();
|
||||
}
|
||||
|
||||
let title = durations.shift();
|
||||
for (const duration of durations.slice(0, 2)) {
|
||||
title += ' | ' + duration;
|
||||
}
|
||||
document.title = title;
|
||||
}
|
||||
|
||||
_setEntries(entries) {
|
||||
/**
|
||||
* Adapts the ticktac menus accordingg to the given entries (amount and duration).
|
||||
* Does not influence listing pages, as those refresh themselves.
|
||||
*
|
||||
* @param {array} entries
|
||||
* @private
|
||||
*/
|
||||
_setEntries(entries)
|
||||
{
|
||||
const hasEntries = entries.length > 0;
|
||||
|
||||
this._menu.style.display = hasEntries ? 'inline-block' : 'none';
|
||||
if (!hasEntries) {
|
||||
// make sure that template entries in the menu are removed, otherwise they
|
||||
// might still be shown in the browsers title
|
||||
for (let record of this._menu.querySelectorAll('[data-since]')) {
|
||||
record.dataset['since'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
const menuEmpty = document.querySelector(this._selectorEmpty);
|
||||
if (menuEmpty !== null) {
|
||||
// these contain the "start" button
|
||||
for (let menuEmpty of document.querySelectorAll(this._selectorEmpty)) {
|
||||
menuEmpty.style.display = !hasEntries ? 'inline-block' : 'none';
|
||||
}
|
||||
|
||||
const stop = this._menu.querySelector('.ticktac-stop');
|
||||
|
||||
if (!hasEntries) {
|
||||
if (stop) {
|
||||
stop.accesskey = null;
|
||||
// and they contain the "stop" button
|
||||
for (let menu of document.querySelectorAll(this._selector)) {
|
||||
menu.style.display = hasEntries ? 'inline-block' : 'none';
|
||||
if (!hasEntries) {
|
||||
// make sure that template entries in the menu are removed, otherwise they
|
||||
// might still be shown in the browsers title
|
||||
for (let record of menu.querySelectorAll('[data-since]')) {
|
||||
record.dataset['since'] = '';
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
const stop = menu.querySelector('.ticktac-stop');
|
||||
|
||||
if (!hasEntries) {
|
||||
if (stop) {
|
||||
stop.accesskey = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stop) {
|
||||
stop.accesskey = 's';
|
||||
}
|
||||
this._replaceInNode(menu, entries[0]);
|
||||
}
|
||||
|
||||
if (stop) {
|
||||
stop.accesskey = 's';
|
||||
}
|
||||
this._replaceInNode(this._menu, entries[0]);
|
||||
this._updateDuration();
|
||||
}
|
||||
|
||||
_replaceInNode(node, timesheet) {
|
||||
/**
|
||||
* @param {HTMLElement} node
|
||||
* @param {object} timesheet
|
||||
* @private
|
||||
*/
|
||||
_replaceInNode(node, timesheet)
|
||||
{
|
||||
const date = this.getDateUtils();
|
||||
const allReplacer = node.querySelectorAll('[data-replacer]');
|
||||
for (let link of allReplacer) {
|
||||
const replacerName = link.dataset['replacer'];
|
||||
if (replacerName === 'url') {
|
||||
link.href = this.attributes['href'].replace('000', timesheet.id);
|
||||
link.href = node.dataset['href'].replace('000', timesheet.id);
|
||||
} else if (replacerName === 'activity') {
|
||||
link.innerText = timesheet.activity.name;
|
||||
} else if (replacerName === 'project') {
|
||||
@@ -158,11 +182,15 @@ export default class KimaiActiveRecords extends KimaiPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
reloadActiveRecords() {
|
||||
reloadActiveRecords()
|
||||
{
|
||||
/** @type {KimaiAPI} API */
|
||||
const API = this.getContainer().getPlugin('api');
|
||||
|
||||
API.get(this.attributes['api'], {}, (result) => {
|
||||
// TODO using the first found "ticktac" menu is working, but can be done better
|
||||
const apiUrl = document.querySelector(this._selector).dataset['api'];
|
||||
|
||||
API.get(apiUrl, {}, (result) => {
|
||||
this._setEntries(result);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,8 +243,7 @@ export default class KimaiDateUtils extends KimaiPlugin {
|
||||
format = '-' + format;
|
||||
}
|
||||
|
||||
return format.replace('%h', (hours < 10 ? '0' + hours : hours)).replace('%m', ('0' + minutes).slice(-2));
|
||||
//return format.replace('%h', (hours < 10 ? '0' + hours : hours)).replace('%m', ('0' + minutes).slice(-2)).replace('%s', ('0' + seconds).slice(-2));
|
||||
return format.replace('%h', hours.toString()).replace('%m', ('0' + minutes).slice(-2));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,15 @@ import KimaiPlugin from '../KimaiPlugin';
|
||||
|
||||
export default class KimaiRecentActivities extends KimaiPlugin {
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this._selector = '.notifications-menu';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getId()
|
||||
{
|
||||
return 'recent-activities';
|
||||
@@ -20,15 +29,17 @@ export default class KimaiRecentActivities extends KimaiPlugin {
|
||||
|
||||
init()
|
||||
{
|
||||
this.menu = document.querySelector('header .notifications-menu');
|
||||
const menus = document.querySelectorAll(this._selector);
|
||||
|
||||
// the menu can be hidden if user has no permissions to see it
|
||||
// or no timesheet was recorded yet
|
||||
if (this.menu === null || this.menu.dataset['reload'] === undefined) {
|
||||
if (menus.length === 0 || menus[0].dataset['reload'] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handle = () => {
|
||||
this._reloadMenu(this.menu.dataset['reload']);
|
||||
// TODO this works but using the first menu is not ideal, pass in the reload URL?
|
||||
this._reloadMenu(menus[0].dataset['reload']);
|
||||
};
|
||||
|
||||
document.addEventListener('kimai.recentActivities', handle);
|
||||
@@ -44,9 +55,12 @@ export default class KimaiRecentActivities extends KimaiPlugin {
|
||||
this._attachAddRemoveFavorite();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_attachAddRemoveFavorite()
|
||||
{
|
||||
[].slice.call(this.menu.querySelectorAll('a.list-group-item-actions')).map((element) => {
|
||||
[].slice.call(document.querySelectorAll(this._selector + ' a.list-group-item-actions')).map((element) => {
|
||||
element.addEventListener('click', (event) => {
|
||||
this._reloadMenu(event.currentTarget.href);
|
||||
|
||||
@@ -58,26 +72,33 @@ export default class KimaiRecentActivities extends KimaiPlugin {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload all ercent activities and update the existing menus.
|
||||
*
|
||||
* @param {string} url
|
||||
* @private
|
||||
*/
|
||||
_reloadMenu(url)
|
||||
{
|
||||
this.fetch(url, {method: 'GET'})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
//this.menu.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
return response.text().then(html => {
|
||||
const newFormHtml = document.createElement('div');
|
||||
newFormHtml.innerHTML = html;
|
||||
this.menu.replaceWith(newFormHtml.firstElementChild);
|
||||
|
||||
this.menu = document.querySelector('header .notifications-menu');
|
||||
for (let menu of document.querySelectorAll(this._selector)) {
|
||||
menu.replaceWith(newFormHtml.firstElementChild.cloneNode(true));
|
||||
}
|
||||
|
||||
this._attachAddRemoveFavorite();
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
//this.menu.remove();
|
||||
.catch((reason) => {
|
||||
console.log('Failed to log recent activities', reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
8
assets/sass/fontawesome.scss
vendored
8
assets/sass/fontawesome.scss
vendored
@@ -6,10 +6,10 @@
|
||||
*/
|
||||
|
||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts/";
|
||||
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
|
||||
@import '~@fortawesome/fontawesome-free/scss/regular';
|
||||
@import '~@fortawesome/fontawesome-free/scss/solid';
|
||||
@import '~@fortawesome/fontawesome-free/scss/brands';
|
||||
@import "@fortawesome/fontawesome-free/scss/fontawesome";
|
||||
@import "@fortawesome/fontawesome-free/scss/regular";
|
||||
@import "@fortawesome/fontawesome-free/scss/solid";
|
||||
@import "@fortawesome/fontawesome-free/scss/brands";
|
||||
|
||||
/* all icon-buttons with fontawesome icons */
|
||||
.btn-icon i.icon {
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@import "~bootstrap/scss/functions";
|
||||
@import "~bootstrap/scss/variables";
|
||||
@import "~bootstrap/scss/maps";
|
||||
@import "~bootstrap/scss/mixins";
|
||||
@import "~bootstrap/scss/reboot";
|
||||
@import "~bootstrap/scss/type";
|
||||
@import "~bootstrap/scss/grid";
|
||||
@import "~bootstrap/scss/tables";
|
||||
@import "~bootstrap/scss/list-group";
|
||||
@import "~bootstrap/scss/card";
|
||||
@import "~bootstrap/scss/utilities";
|
||||
@import "bootstrap/scss/functions";
|
||||
@import "bootstrap/scss/variables";
|
||||
@import "bootstrap/scss/maps";
|
||||
@import "bootstrap/scss/mixins";
|
||||
@import "bootstrap/scss/reboot";
|
||||
@import "bootstrap/scss/type";
|
||||
@import "bootstrap/scss/grid";
|
||||
@import "bootstrap/scss/tables";
|
||||
@import "bootstrap/scss/list-group";
|
||||
@import "bootstrap/scss/card";
|
||||
@import "bootstrap/scss/utilities";
|
||||
|
||||
body {
|
||||
font-family: $font-family-sans-serif;
|
||||
|
||||
@@ -5,11 +5,22 @@
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.notifications-menu .dropdown-menu-card {
|
||||
width: 100vw;
|
||||
right: 0;
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 360px) {
|
||||
.inline-search {
|
||||
max-width: 235px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
.inline-search {
|
||||
max-width: 325px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '~tom-select/dist/scss/tom-select.bootstrap5';
|
||||
@import "tom-select/dist/scss/tom-select.bootstrap5";
|
||||
|
||||
/* bootstrap thead.sticky-top has z-index 1020 and tomselect dropdowns often hide behind that */
|
||||
.ts-dropdown {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '~@tabler/core/src/scss/variables';
|
||||
@import "@tabler/core/src/scss/variables";
|
||||
|
||||
/* fix for vertical labels in date-time pickers, if they are used in half-column forms, e.g. "create tasks" or "create expenses" */
|
||||
fieldset legend.col-form-label {
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
.ticktac-stop {
|
||||
i {
|
||||
animation: ticktac-blink 2s step-end infinite;
|
||||
font-size: 140%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@import "~bootstrap/scss/functions";
|
||||
@import "~bootstrap/scss/variables";
|
||||
@import "~bootstrap/scss/mixins";
|
||||
@import "bootstrap/scss/functions";
|
||||
@import "bootstrap/scss/variables";
|
||||
@import "bootstrap/scss/mixins";
|
||||
|
||||
@import '~@tabler/core/src/scss/variables';
|
||||
@import '~@tabler/core/src/scss/mixins';
|
||||
@import "@tabler/core/src/scss/variables";
|
||||
@import "@tabler/core/src/scss/mixins";
|
||||
|
||||
/* For highlighting rows, fieldsets and so on */
|
||||
$highlight-bg: #f5f5f5;
|
||||
|
||||
Reference in New Issue
Block a user