Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ RUN \
ENV APP_ENV=dev
ENV DATABASE_URL=
ENV memory_limit=512M
# Increase Apache request header limits to handle large cookies/tokens
RUN echo "LimitRequestFieldSize 32768" >> /etc/apache2/apache2.conf && \
echo "LimitRequestLine 32768" >> /etc/apache2/apache2.conf

# the "prod" stage (production build) is configured as last stage in the file, as this is the default target in BuildKit
FROM base AS prod
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ FROM kimai/kimai2:apache
COPY var/plugins/NotionIntegrationBundle /opt/kimai/var/plugins/NotionIntegrationBundle
COPY var/plugins/CustomerPortalBundle /opt/kimai/var/plugins/CustomerPortalBundle

# Copy customized templates (if you have any local changes)
COPY templates /opt/kimai/templates

# Set proper permissions
RUN chown -R www-data:www-data /opt/kimai/var/plugins
RUN chown -R www-data:www-data /opt/kimai/var/plugins /opt/kimai/templates

# Note: We don't run cache reload here because the database isn't available at build time.
# The official kimai entrypoint will handle cache warming on container start.
1 change: 1 addition & 0 deletions assets/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ global.KimaiPaginatedBoxWidget = require('./js/widgets/KimaiPaginatedBoxWidget')
global.KimaiReloadPageWidget = require('./js/widgets/KimaiReloadPageWidget').default;
global.KimaiColor = require('./js/widgets/KimaiColor').default;
global.KimaiStorage = require('./js/widgets/KimaiStorage').default;
global.TagAutoFill = require('./js/widgets/TagAutoFill').default;
57 changes: 57 additions & 0 deletions assets/js/forms/KimaiTimesheetForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
delete this._activity;
}

if (this._tags !== undefined && this._tagsListener !== undefined) {
this._tags.removeEventListener('change', this._tagsListener);
delete this._tagsListener;
delete this._tags;
}

if (this._description !== undefined) {
delete this._description;
}

if (this._project !== undefined) {
delete this._project;
}
Expand All @@ -91,6 +101,8 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {

this._activity = document.getElementById(formPrefix + '_activity');
this._project = document.getElementById(formPrefix + '_project');
this._tags = document.getElementById(formPrefix + '_tags');
this._description = document.getElementById(formPrefix + '_description');

/** @param {CustomEvent} event */
this._activityListener = (event) => {
Expand All @@ -107,6 +119,12 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
};
this._activity.addEventListener('create', this._activityListener);

// Auto-fill description from tags
if (this._tags && this._description) {
this._tagsListener = () => this._autoFillDescriptionFromTags();
this._tags.addEventListener('change', this._tagsListener);
}

this._beginDate = document.getElementById(formPrefix + '_begin_date');
this._beginTime = document.getElementById(formPrefix + '_begin_time');
this._endTime = document.getElementById(formPrefix + '_end_time');
Expand Down Expand Up @@ -695,4 +713,43 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
timeField.setSelectionRange(cursorPos, cursorPos);
}, 0);
}

/**
* Auto-fill description field from tags when tags are added
* Only fills if description is empty
* @private
*/
_autoFillDescriptionFromTags()
{
if (!this._tags || !this._description) {
return;
}

// Only auto-fill if description is empty
if (this._description.value.trim() !== '') {
return;
}

// Get the tag values - TomSelect stores values differently
let tagValues = [];

// Check if TomSelect is initialized on the tags field
if (this._tags.tomselect) {
tagValues = this._tags.tomselect.getValue();
if (typeof tagValues === 'string') {
tagValues = tagValues.split(',').filter(t => t.trim() !== '');
}
} else {
// Fallback to regular select value
const selectedOptions = Array.from(this._tags.selectedOptions || []);
tagValues = selectedOptions.map(option => option.text || option.value);
}

// Join tags with comma and space, and set as description
if (tagValues.length > 0) {
this._description.value = tagValues.join(', ');
// Trigger change event so any listeners are notified
this._description.dispatchEvent(new Event('change', { bubbles: true }));
}
}
}
153 changes: 86 additions & 67 deletions assets/js/plugins/KimaiThemeInitializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,84 +9,103 @@
* [KIMAI] KimaiThemeInitializer: initialize theme functionality
*/

import { Tooltip, Offcanvas } from 'bootstrap';
import KimaiPlugin from '../KimaiPlugin';
import { Tooltip, Offcanvas } from "bootstrap";
import KimaiPlugin from "../KimaiPlugin";

export default class KimaiThemeInitializer extends KimaiPlugin {
init() {
// the tooltip do not use data-bs-toggle="tooltip" so they can be mixed with data-toggle="modal"
// Fix: Add proper cleanup to prevent tooltips from sticking
[].slice
.call(document.querySelectorAll('[data-toggle="tooltip"]'))
.map(function (tooltipTriggerEl) {
const tooltip = new Tooltip(tooltipTriggerEl, {
trigger: "hover",
delay: { show: 500, hide: 100 },
});

init()
{
// the tooltip do not use data-bs-toggle="tooltip" so they can be mixed with data-toggle="modal"
[].slice.call(document.querySelectorAll('[data-toggle="tooltip"]')).map(function (tooltipTriggerEl) {
return new Tooltip(tooltipTriggerEl);
// Force hide tooltip when mouse leaves
tooltipTriggerEl.addEventListener("mouseleave", function () {
tooltip.hide();
});

// support for offcanvas elements
const offcanvasElementList = document.querySelectorAll('.offcanvas');
[...offcanvasElementList].map(offcanvasEl => new Offcanvas(offcanvasEl));
// Cleanup tooltip when element is removed from DOM
tooltipTriggerEl.addEventListener("DOMNodeRemoved", function () {
tooltip.dispose();
});

// activate all form plugins
/** @type {KimaiForm} FORMS */
const FORMS = this.getContainer().getPlugin('form');
FORMS.activateForm('div.page-wrapper form');
return tooltip;
});

this._registerModalAutofocus('#remote_form_modal');
// support for offcanvas elements
const offcanvasElementList = document.querySelectorAll(".offcanvas");
[...offcanvasElementList].map((offcanvasEl) => new Offcanvas(offcanvasEl));

this.overlay = null;
// activate all form plugins
/** @type {KimaiForm} FORMS */
const FORMS = this.getContainer().getPlugin("form");
FORMS.activateForm("div.page-wrapper form");

// register a global event listener, which displays an overlays upon notification
document.addEventListener('kimai.reloadContent', (event) => {
// do not allow more than one loading screen at a time
if (this.overlay !== null) {
return;
}

// at which element we append the loading screen
let container = 'body';
if (event.detail !== undefined && event.detail !== null) {
container = event.detail;
}

const temp = document.createElement('div');
temp.innerHTML = '<div class="overlay"><div class="fas fa-sync fa-spin"></div></div>';
this.overlay = temp.firstElementChild;
document.querySelector(container).append(this.overlay);
});
this._registerModalAutofocus("#remote_form_modal");

// register a global event listener, which hides an overlay upon notification
document.addEventListener('kimai.reloadedContent', () => {
if (this.overlay !== null) {
this.overlay.remove();
this.overlay = null;
}
});
this.overlay = null;

// register a global event listener, which displays an overlays upon notification
document.addEventListener("kimai.reloadContent", (event) => {
// do not allow more than one loading screen at a time
if (this.overlay !== null) {
return;
}

// at which element we append the loading screen
let container = "body";
if (event.detail !== undefined && event.detail !== null) {
container = event.detail;
}

const temp = document.createElement("div");
temp.innerHTML =
'<div class="overlay"><div class="fas fa-sync fa-spin"></div></div>';
this.overlay = temp.firstElementChild;
document.querySelector(container).append(this.overlay);
});

// register a global event listener, which hides an overlay upon notification
document.addEventListener("kimai.reloadedContent", () => {
if (this.overlay !== null) {
this.overlay.remove();
this.overlay = null;
}
});
}

/**
* Helps to set the autofocus on modals.
*
* @param {string} selector
*/
_registerModalAutofocus(selector) {
// on mobile you do not want to trigger the virtual keyboard upon modal open
if (this.isMobile()) {
return;
}

/**
* Helps to set the autofocus on modals.
*
* @param {string} selector
*/
_registerModalAutofocus(selector) {
// on mobile you do not want to trigger the virtual keyboard upon modal open
if (this.isMobile()) {
return;
}

const modal = document.querySelector(selector);
if (modal === null) {
return;
}

modal.addEventListener('shown.bs.modal', () => {
const form = modal.querySelector('form');
let formAutofocus = form.querySelectorAll('[autofocus]');
if (formAutofocus.length < 1) {
formAutofocus = form.querySelectorAll('input[type=text],input[type=date],textarea,select');
}
if (formAutofocus.length > 0) {
formAutofocus[0].focus();
}
});
const modal = document.querySelector(selector);
if (modal === null) {
return;
}

modal.addEventListener("shown.bs.modal", () => {
const form = modal.querySelector("form");
let formAutofocus = form.querySelectorAll("[autofocus]");
if (formAutofocus.length < 1) {
formAutofocus = form.querySelectorAll(
"input[type=text],input[type=date],textarea,select"
);
}
if (formAutofocus.length > 0) {
formAutofocus[0].focus();
}
});
}
}
Loading