Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2ce42523a | ||
|
|
b2dc5b1035 | ||
|
|
752d0cfbf3 | ||
|
|
32c8ec2aaf | ||
|
|
2ffdaa088f | ||
|
|
2fafaabe40 | ||
|
|
b1d86b4836 | ||
|
|
384fe8b8ab | ||
|
|
d429c56687 | ||
|
|
8c52eac9f0 | ||
|
|
59ecbbb591 | ||
|
|
2e54a56996 | ||
|
|
6b2a56f492 | ||
|
|
5d48d463d9 | ||
|
|
f295d6e30e | ||
|
|
162acf37a8 | ||
|
|
37c741c48d | ||
|
|
bbdcf16a5d | ||
|
|
dfb0134137 | ||
|
|
6a86afb5fd | ||
|
|
677f8d776c | ||
|
|
9e87fc131b | ||
|
|
5a0c783a76 | ||
|
|
26361da873 | ||
|
|
26f8abfcc6 | ||
|
|
27c2b912f7 | ||
|
|
7e72f144cb | ||
|
|
3c3d6379a8 | ||
|
|
806bb97e60 | ||
|
|
3fcaf55d02 | ||
|
|
f0e2337d67 | ||
|
|
00d801616a | ||
|
|
afb5a0bba4 | ||
|
|
8c1ed68817 | ||
|
|
b61420d633 | ||
|
|
595b5f4b25 | ||
|
|
07ac6c308f | ||
|
|
a6766373b4 | ||
|
|
a15c1e56cb | ||
|
|
8a42c9640f | ||
|
|
8e6764b67a | ||
|
|
ea29799b89 | ||
|
|
a3ef1fd1cd | ||
|
|
1a38c7d7a3 | ||
|
|
636a51e721 | ||
|
|
cc64acf0f8 | ||
|
|
76821c24ed | ||
|
|
190b1dd4f3 | ||
|
|
7937fa281a | ||
|
|
6d78c6ba36 | ||
|
|
5d3e46cce2 | ||
|
|
1c3aac78f6 | ||
|
|
b1fb300a71 | ||
|
|
a4d658b821 | ||
|
|
4920ea5075 | ||
|
|
03dbbdc5ba | ||
|
|
023048e7c0 | ||
|
|
24778d3ffb | ||
|
|
04331420ae | ||
|
|
f3890691ce | ||
|
|
dfec807166 | ||
|
|
06b3060fe1 | ||
|
|
b6a8ae783f | ||
|
|
3d8fa3af44 | ||
|
|
59dc82351e | ||
|
|
da10099248 | ||
|
|
05aaa1950a | ||
|
|
46129c7ab9 | ||
|
|
c8b4e4eabb | ||
|
|
2f2ebd6293 | ||
|
|
594385f741 | ||
|
|
12190141d7 | ||
|
|
0420eb27c7 | ||
|
|
81107377f4 | ||
|
|
e9c172daea | ||
|
|
f42d42f512 | ||
|
|
b38c6fbb0e | ||
|
|
e2a1146670 | ||
|
|
e69ac6968b | ||
|
|
87168de4f1 | ||
|
|
7b158d57b5 | ||
|
|
1e0fbf0b73 | ||
|
|
6e26a37c4d | ||
|
|
fac5a910d8 | ||
|
|
dfd97fd6f3 | ||
|
|
452a8d9390 | ||
|
|
2a9de506a1 | ||
|
|
e663cc2b02 | ||
|
|
a70c803158 | ||
|
|
9ca69e1114 | ||
|
|
2e6b700b43 | ||
|
|
2a75cd6230 | ||
|
|
934fdeb107 | ||
|
|
14cdcd3f63 | ||
|
|
674bf3a6b5 | ||
|
|
cce49cc409 | ||
|
|
04cb403ea9 | ||
|
|
afaa845a9a | ||
|
|
928b5208e9 | ||
|
|
8a8d0503f7 | ||
|
|
b341358d0a | ||
|
|
d30166e691 | ||
|
|
b42c77a2a1 | ||
|
|
8444928ae4 | ||
|
|
b250edc2a1 | ||
|
|
4a8c2a79c9 | ||
|
|
bff72b30cc | ||
|
|
9d7b418d99 | ||
|
|
f2fb338539 | ||
|
|
1b3ff212d2 | ||
|
|
c07979e3a9 | ||
|
|
23e8dcf495 | ||
|
|
4332ef95a2 | ||
|
|
4fdfb6f478 | ||
|
|
136104d4b0 | ||
|
|
c7f0508707 | ||
|
|
9bd37fb695 | ||
|
|
21c031f2c8 | ||
|
|
17a815e5a9 | ||
|
|
648686c001 | ||
|
|
82a3b99a31 | ||
|
|
70741eebfd | ||
|
|
eb933e7021 | ||
|
|
372f3c8507 | ||
|
|
e030ff08db | ||
|
|
f13b81ede7 | ||
|
|
46c4449504 | ||
|
|
8b55cd3693 | ||
|
|
406ac7b9cb | ||
|
|
0c26a2678e | ||
|
|
49eb7068c9 | ||
|
|
dcc52f1a95 | ||
|
|
31bae44f3c | ||
|
|
84a0ca5ce6 | ||
|
|
9e736b26f6 | ||
|
|
32a1306394 | ||
|
|
96043afd6a | ||
|
|
255c7d77d6 | ||
|
|
e6d5a720bf | ||
|
|
ff9bf163ee | ||
|
|
5fac6a642c | ||
|
|
b1b81ee8a6 | ||
|
|
1ea64bf18e | ||
|
|
0c4fdda00a | ||
|
|
215bd6a01a | ||
|
|
63b8164926 | ||
|
|
8c26ff9aed | ||
|
|
921a703ea2 | ||
|
|
855a546683 | ||
|
|
52b949daa3 | ||
|
|
ee040d29b6 | ||
|
|
fb9a0dc499 | ||
|
|
40154be8f9 | ||
|
|
13649e146c | ||
|
|
4076e1c3d3 | ||
|
|
8de54e1fa7 | ||
|
|
1965c35b43 | ||
|
|
3fd770047e | ||
|
|
036b297a76 | ||
|
|
537c120ad9 | ||
|
|
3e0dadc0c8 | ||
|
|
004ac5a6b9 | ||
|
|
3204dcb03e | ||
|
|
b1903ba183 | ||
|
|
75fa7a20b8 | ||
|
|
a19426147a | ||
|
|
9d933f62c0 | ||
|
|
9e3d243b4b | ||
|
|
62047c7c01 | ||
|
|
0bcd45babe | ||
|
|
2a72815481 | ||
|
|
ff6a3e8262 | ||
|
|
0947b88d36 | ||
|
|
5de5b4fef5 | ||
|
|
cde856a9c1 | ||
|
|
6f8877a740 | ||
|
|
e7d71bff64 | ||
|
|
cca0657291 | ||
|
|
a4904d3335 | ||
|
|
b9c1036b21 | ||
|
|
818c080d28 | ||
|
|
a5a6ae065b | ||
|
|
8b06ca9d10 | ||
|
|
7e1025d61d | ||
|
|
8788311faf | ||
|
|
ce22520d7f | ||
|
|
f5d76cfd9a | ||
|
|
27f7f14539 | ||
|
|
f7c32e633a | ||
|
|
6d9055826f | ||
|
|
4627a9047e |
@@ -2,14 +2,12 @@
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /opt/kimai/public
|
||||
|
||||
PassEnv DATABASE_PREFIX
|
||||
PassEnv MAILER_FROM
|
||||
PassEnv APP_ENV
|
||||
PassEnv APP_SECRET
|
||||
PassEnv DATABASE_URL
|
||||
PassEnv MAILER_URL
|
||||
PassEnv TRUSTED_PROXIES
|
||||
PassEnv TRUSTED_HOSTS
|
||||
|
||||
<Directory "/opt/kimai/public">
|
||||
Require all granted
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
# Source base [fpm/apache]
|
||||
ARG BASE="fpm"
|
||||
ARG PHP_VER="8.2"
|
||||
ARG COMPOSER_VER="latest"
|
||||
ARG TIMEZONE="Europe/London"
|
||||
|
||||
###########################
|
||||
# Shared tools
|
||||
###########################
|
||||
|
||||
# composer base image
|
||||
FROM composer:${COMPOSER_VER} AS composer
|
||||
|
||||
###########################
|
||||
# PHP extensions
|
||||
###########################
|
||||
|
||||
#fpm alpine php extension base
|
||||
FROM php:${PHP_VER}-fpm-alpine AS fpm-php-ext-base
|
||||
RUN apk add --no-cache \
|
||||
# build-tools
|
||||
autoconf \
|
||||
dpkg \
|
||||
dpkg-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
icu-dev \
|
||||
libatomic \
|
||||
libc-dev \
|
||||
libgomp \
|
||||
libmagic \
|
||||
m4 \
|
||||
make \
|
||||
mpc1 \
|
||||
mpfr4 \
|
||||
musl-dev \
|
||||
perl \
|
||||
re2c \
|
||||
# gd
|
||||
freetype-dev \
|
||||
libpng-dev \
|
||||
# icu
|
||||
icu-dev \
|
||||
icu-data-full \
|
||||
# ldap
|
||||
openldap-dev \
|
||||
libldap \
|
||||
# zip
|
||||
libzip-dev \
|
||||
# xsl
|
||||
libxslt-dev
|
||||
|
||||
|
||||
# apache debian php extension base
|
||||
FROM php:${PHP_VER}-apache-bookworm AS apache-php-ext-base
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y \
|
||||
libldap2-dev \
|
||||
libicu-dev \
|
||||
libpng-dev \
|
||||
libzip-dev \
|
||||
libxslt1-dev \
|
||||
libfreetype6-dev
|
||||
|
||||
|
||||
# php extension gd - 13.86s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-gd
|
||||
RUN docker-php-ext-configure gd \
|
||||
--with-freetype && \
|
||||
docker-php-ext-install -j$(nproc) gd
|
||||
|
||||
# php extension intl : 15.26s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-intl
|
||||
RUN docker-php-ext-install -j$(nproc) intl
|
||||
|
||||
# php extension ldap : 8.45s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-ldap
|
||||
RUN docker-php-ext-configure ldap && \
|
||||
docker-php-ext-install -j$(nproc) ldap
|
||||
|
||||
# php extension pdo_mysql : 6.14s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-pdo_mysql
|
||||
RUN docker-php-ext-install -j$(nproc) pdo_mysql
|
||||
|
||||
# php extension zip : 8.18s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-zip
|
||||
RUN docker-php-ext-install -j$(nproc) zip
|
||||
|
||||
# php extension xsl : ?.?? s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-xsl
|
||||
RUN docker-php-ext-install -j$(nproc) xsl
|
||||
|
||||
# php extension redis
|
||||
FROM ${BASE}-php-ext-base AS php-ext-redis
|
||||
RUN yes no | pecl install redis && \
|
||||
docker-php-ext-enable redis
|
||||
|
||||
# php extension opcache
|
||||
FROM ${BASE}-php-ext-base AS php-ext-opcache
|
||||
RUN docker-php-ext-install -j$(nproc) opcache
|
||||
|
||||
###########################
|
||||
# fpm base build
|
||||
###########################
|
||||
|
||||
# fpm base build
|
||||
FROM php:${PHP_VER}-fpm-alpine AS fpm-base
|
||||
ARG TIMEZONE
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
coreutils \
|
||||
freetype \
|
||||
haveged \
|
||||
icu \
|
||||
icu-data-full \
|
||||
libldap \
|
||||
libpng \
|
||||
libzip \
|
||||
libxslt-dev \
|
||||
fcgi \
|
||||
tzdata && \
|
||||
touch /use_fpm
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
||||
CMD \
|
||||
SCRIPT_NAME=/ping \
|
||||
SCRIPT_FILENAME=/ping \
|
||||
REQUEST_METHOD=GET \
|
||||
cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1
|
||||
|
||||
|
||||
|
||||
###########################
|
||||
# apache base build
|
||||
###########################
|
||||
|
||||
FROM php:${PHP_VER}-apache-bookworm AS apache-base
|
||||
ARG TIMEZONE
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
bash \
|
||||
haveged \
|
||||
libicu72 \
|
||||
libldap-common \
|
||||
libpng16-16 \
|
||||
libzip4 \
|
||||
libxslt1.1 \
|
||||
libfreetype6 && \
|
||||
echo "Listen 8001" > /etc/apache2/ports.conf && \
|
||||
a2enmod rewrite && \
|
||||
touch /use_apache
|
||||
|
||||
EXPOSE 8001
|
||||
|
||||
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
||||
CMD curl -f http://127.0.0.1:8001 || exit 1
|
||||
|
||||
|
||||
|
||||
###########################
|
||||
# global base build
|
||||
###########################
|
||||
|
||||
FROM ${BASE}-base AS base
|
||||
ARG TIMEZONE
|
||||
|
||||
LABEL maintainer="tobias@neontribe.co.uk"
|
||||
|
||||
ENV TIMEZONE=${TIMEZONE}
|
||||
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone && \
|
||||
# make composer home dir
|
||||
mkdir /composer && \
|
||||
chown -R www-data:www-data /composer && \
|
||||
echo "echo -e This imnage is not intended to be used stand alone. It's a base image\necho for the Kimai project. https://www.kimai.org\n" > /entrypoint.sh && \
|
||||
chmod 777 /entrypoint.sh
|
||||
# copy composer
|
||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# copy php extensions
|
||||
|
||||
# PHP extension xsl
|
||||
COPY --from=php-ext-xsl /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini
|
||||
COPY --from=php-ext-xsl /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xsl.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xsl.so
|
||||
# PHP extension pdo_mysql
|
||||
COPY --from=php-ext-pdo_mysql /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini
|
||||
COPY --from=php-ext-pdo_mysql /usr/local/lib/php/extensions/no-debug-non-zts-20220829/pdo_mysql.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/pdo_mysql.so
|
||||
# PHP extension zip
|
||||
COPY --from=php-ext-zip /usr/local/etc/php/conf.d/docker-php-ext-zip.ini /usr/local/etc/php/conf.d/docker-php-ext-zip.ini
|
||||
COPY --from=php-ext-zip /usr/local/lib/php/extensions/no-debug-non-zts-20220829/zip.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/zip.so
|
||||
# PHP extension ldap
|
||||
COPY --from=php-ext-ldap /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini
|
||||
COPY --from=php-ext-ldap /usr/local/lib/php/extensions/no-debug-non-zts-20220829/ldap.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/ldap.so
|
||||
# PHP extension gd
|
||||
COPY --from=php-ext-gd /usr/local/etc/php/conf.d/docker-php-ext-gd.ini /usr/local/etc/php/conf.d/docker-php-ext-gd.ini
|
||||
COPY --from=php-ext-gd /usr/local/lib/php/extensions/no-debug-non-zts-20220829/gd.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/gd.so
|
||||
# PHP extension intl
|
||||
COPY --from=php-ext-intl /usr/local/etc/php/conf.d/docker-php-ext-intl.ini /usr/local/etc/php/conf.d/docker-php-ext-intl.ini
|
||||
COPY --from=php-ext-intl /usr/local/lib/php/extensions/no-debug-non-zts-20220829/intl.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/intl.so
|
||||
# PHP extension redis
|
||||
COPY --from=php-ext-redis /usr/local/etc/php/conf.d/docker-php-ext-redis.ini /usr/local/etc/php/conf.d/docker-php-ext-redis.ini
|
||||
COPY --from=php-ext-redis /usr/local/lib/php/extensions/no-debug-non-zts-20220829/redis.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/redis.so
|
||||
COPY --from=php-ext-opcache /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
|
||||
|
||||
CMD [ "/entrypoint.sh" ]
|
||||
|
||||
# docker build -t kimai/kimai-base:fpm --build-arg BASE=fpm .
|
||||
# docker build -t kimai/kimai-base:apache --build-arg BASE=apache .
|
||||
@@ -1,35 +1,31 @@
|
||||
<?php
|
||||
$DB_HOST = $argv[1];
|
||||
$DB_BASE = $argv[2];
|
||||
$DB_PORT = $argv[3];
|
||||
$DB_USER = $argv[4];
|
||||
$DB_PASS = $argv[5];
|
||||
$DATABASE_HOST = urldecode($argv[1]);
|
||||
$DATABASE_BASE = urldecode($argv[2]);
|
||||
$DATABASE_PORT = $argv[3];
|
||||
$DATABASE_USER = urldecode($argv[4]);
|
||||
$DATABASE_PASS = urldecode($argv[5]);
|
||||
|
||||
echo "Testing DB:";
|
||||
echo "*";
|
||||
echo "* new \PDO(mysql:host=$DB_HOST;dbname=$DB_BASE;port=$DB_PORT, $DB_USER, $DB_PASS, [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION ]);";
|
||||
echo "*";
|
||||
|
||||
try {
|
||||
$pdo = new \PDO("mysql:host=$DB_HOST;dbname=$DB_BASE;port=$DB_PORT", "$DB_USER", "$DB_PASS", [
|
||||
$pdo = new \PDO("mysql:host=$DATABASE_HOST;dbname=$DATABASE_BASE;port=$DATABASE_PORT", "$DATABASE_USER", "$DATABASE_PASS", [
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
|
||||
]);
|
||||
} catch(\Exception $ex) {
|
||||
switch ($ex->getCode()) {
|
||||
// we can immediately stop startup here and show the error message
|
||||
case 1045:
|
||||
// we can immediately stop here and show the error message
|
||||
echo 'Access denied (1045)';
|
||||
die(1);
|
||||
// we can immediately stop startup here and show the error message
|
||||
case 1049:
|
||||
echo 'Unknown database (1049)';
|
||||
die(2);
|
||||
// error "Unknown database (1049)" can be ignored, the database will be created by Kimai
|
||||
return;
|
||||
// a lot of errors share the same meaningless error code zero
|
||||
case 0:
|
||||
// this error includes the database name, so we can only search for the static part of the error message
|
||||
if (stripos($ex->getMessage(), 'SQLSTATE[HY000] [1049] Unknown database') !== false) {
|
||||
echo 'Unknown database (0-1049)';
|
||||
die(3);
|
||||
// error "Unknown database (1049)" can be ignored, the database will be created by Kimai
|
||||
return;
|
||||
}
|
||||
switch ($ex->getMessage()) {
|
||||
// eg. no response (fw) - the startup script should retry it a couple of times
|
||||
|
||||
@@ -3,39 +3,21 @@
|
||||
KIMAI=$(cat /opt/kimai/version.txt)
|
||||
echo $KIMAI
|
||||
|
||||
|
||||
function config() {
|
||||
# set mem limits and copy in custom logger config
|
||||
if [ -z "$memory_limit" ]; then
|
||||
memory_limit=256M
|
||||
fi
|
||||
|
||||
|
||||
function waitForDB() {
|
||||
# Parse sql connection data
|
||||
if [ ! -z "$DATABASE_URL" ]; then
|
||||
DB_TYPE=$(awk -F '[/:@]' '{print $1}' <<< "$DATABASE_URL")
|
||||
DB_USER=$(awk -F '[/:@]' '{print $4}' <<< "$DATABASE_URL")
|
||||
DB_PASS=$(awk -F '[/:@]' '{print $5}' <<< "$DATABASE_URL")
|
||||
DB_HOST=$(awk -F '[/:@]' '{print $6}' <<< "$DATABASE_URL")
|
||||
DB_PORT=$(awk -F '[/:@]' '{print $7}' <<< "$DATABASE_URL")
|
||||
DB_BASE=$(awk -F '[/?]' '{print $4}' <<< "$DATABASE_URL")
|
||||
else
|
||||
DB_TYPE=${DB_TYPE:mysql}
|
||||
if [ "$DB_TYPE" == "mysql" ]; then
|
||||
export DATABASE_URL="${DB_TYPE}://${DB_USER:=kimai}:${DB_PASS:=kimai}@${DB_HOST:=sqldb}:${DB_PORT:=3306}/${DB_BASE:=kimai}"
|
||||
else
|
||||
echo "Unknown database type, cannot proceed. Only 'mysql' is supported, received: [$DB_TYPE]"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
DATABASE_USER=$(awk -F '[/:@]' '{print $4}' <<< "$DATABASE_URL")
|
||||
DATABASE_PASS=$(awk -F '[/:@]' '{print $5}' <<< "$DATABASE_URL")
|
||||
DATABASE_HOST=$(awk -F '[/:@]' '{print $6}' <<< "$DATABASE_URL")
|
||||
DATABASE_PORT=$(awk -F '[/:@]' '{print $7}' <<< "$DATABASE_URL")
|
||||
DATABASE_BASE=$(awk -F '[/?]' '{print $4}' <<< "$DATABASE_URL")
|
||||
|
||||
re='^[0-9]+$'
|
||||
if ! [[ $DB_PORT =~ $re ]] ; then
|
||||
DB_PORT=3306
|
||||
if ! [[ $DATABASE_PORT =~ $re ]] ; then
|
||||
DATABASE_PORT=3306
|
||||
fi
|
||||
|
||||
echo "Wait for MySQL DB connection ..."
|
||||
until php /dbtest.php $DB_HOST $DB_BASE $DB_PORT $DB_USER $DB_PASS; do
|
||||
echo "Wait for database connection ..."
|
||||
until php /dbtest.php "$DATABASE_HOST" "$DATABASE_BASE" "$DATABASE_PORT" "$DATABASE_USER" "$DATABASE_PASS"; do
|
||||
echo Checking DB: $?
|
||||
sleep 3
|
||||
done
|
||||
@@ -44,11 +26,12 @@ function config() {
|
||||
|
||||
function handleStartup() {
|
||||
# set mem limits and copy in custom logger config
|
||||
if [ -z "$memory_limit" ]; then
|
||||
memory_limit=512M
|
||||
fi
|
||||
sed -i "s/memory_limit.*/memory_limit=$memory_limit/g" /usr/local/etc/php/php.ini
|
||||
cp /assets/monolog.yaml /opt/kimai/config/packages/monolog.yaml
|
||||
|
||||
tar -zx -C /opt/kimai -f /var/tmp/public.tgz
|
||||
|
||||
if [ -z "$USER_ID" ]; then
|
||||
USER_ID=$(id -u www-data)
|
||||
fi
|
||||
@@ -90,7 +73,30 @@ function handleStartup() {
|
||||
fi
|
||||
}
|
||||
|
||||
config
|
||||
handleStartup
|
||||
function prepareKimai() {
|
||||
# These are idempotent, so we can run them on every start-up
|
||||
/opt/kimai/bin/console -n kimai:install
|
||||
if [ ! -z "$ADMINPASS" ] && [ ! -a "$ADMINMAIL" ]; then
|
||||
/opt/kimai/bin/console kimai:user:create admin "$ADMINMAIL" ROLE_SUPER_ADMIN "$ADMINPASS"
|
||||
fi
|
||||
echo "$KIMAI" > /opt/kimai/var/installed
|
||||
echo "Kimai is ready"
|
||||
}
|
||||
|
||||
exec /service.sh
|
||||
function runServer() {
|
||||
# Just while I'm fixing things
|
||||
/opt/kimai/bin/console kimai:reload --env="$APP_ENV"
|
||||
chown -R $USER_ID:$GROUP_ID /opt/kimai/var
|
||||
if [ -e /use_apache ]; then
|
||||
exec /usr/sbin/apache2 -D FOREGROUND
|
||||
elif [ -e /use_fpm ]; then
|
||||
exec php-fpm
|
||||
else
|
||||
echo "Error, unknown server type"
|
||||
fi
|
||||
}
|
||||
|
||||
waitForDB
|
||||
handleStartup
|
||||
prepareKimai
|
||||
runServer
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
/startup.sh 2>&1
|
||||
echo $$ > /tmp/startup.pid
|
||||
echo "Waiting for kimai to install"
|
||||
while [ ! -f /opt/kimai/var/installed ]; do
|
||||
echo -n ". "
|
||||
sleep 5
|
||||
done
|
||||
echo
|
||||
|
||||
# Test FPM CGI
|
||||
if [ -f /use_fpm ]; then
|
||||
|
||||
export SCRIPT_NAME=/opt/kimai/public/index.php
|
||||
export SCRIPT_FILENAME=/opt/kimai/public/index.php
|
||||
export REQUEST_METHOD=GET
|
||||
export SERVER_ADDR=localhost
|
||||
|
||||
COUNT=0
|
||||
until cgi-fcgi -bind -connect 127.0.0.1:9000 &> /dev/null
|
||||
do
|
||||
COUNT=$((COUNT+1))
|
||||
echo "Waiting for FPM Server to start (${COUNT})"
|
||||
sleep 3
|
||||
if [ "$COUNT" -gt 5 ]; then
|
||||
echo "FPM Failed to start."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
fi
|
||||
|
||||
# Test Apache/httpd
|
||||
if [ -f /use_apache ]; then
|
||||
|
||||
COUNT=0
|
||||
until curl -s -o /dev/null http://localhost:8001
|
||||
do
|
||||
COUNT=$((COUNT+1))
|
||||
echo "Waiting for Apache/HTTP to start (${COUNT})" &> /dev/null
|
||||
sleep 3
|
||||
if [ "$COUNT" -gt 5 ]; then
|
||||
echo "Apache/HTTP failed to start."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
fi
|
||||
|
||||
# Test PHP/Kimai
|
||||
/opt/kimai/bin/console kimai:version
|
||||
if [ $? != 0 ]; then
|
||||
echo "PHP/Kimai not responding"
|
||||
exit 1
|
||||
fi
|
||||
kill $(cat /tmp/startup.pid)
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/bin/bash -x
|
||||
|
||||
function waitForDB() {
|
||||
# Parse sql connection data
|
||||
if [ ! -z "$DATABASE_URL" ]; then
|
||||
DB_TYPE=$(awk -F '[/:@]' '{print $1}' <<< "$DATABASE_URL")
|
||||
DB_USER=$(awk -F '[/:@]' '{print $4}' <<< "$DATABASE_URL")
|
||||
DB_PASS=$(awk -F '[/:@]' '{print $5}' <<< "$DATABASE_URL")
|
||||
DB_HOST=$(awk -F '[/:@]' '{print $6}' <<< "$DATABASE_URL")
|
||||
DB_PORT=$(awk -F '[/:@]' '{print $7}' <<< "$DATABASE_URL")
|
||||
DB_BASE=$(awk -F '[/?]' '{print $4}' <<< "$DATABASE_URL")
|
||||
else
|
||||
DB_TYPE=${DB_TYPE:mysql}
|
||||
if [ "$DB_TYPE" == "mysql" ]; then
|
||||
export DATABASE_URL="${DB_TYPE}://${DB_USER:=kimai}:${DB_PASS:=kimai}@${DB_HOST:=sqldb}:${DB_PORT:=3306}/${DB_BASE:=kimai}"
|
||||
else
|
||||
echo "Unknown database type, cannot proceed. Only 'mysql' is supported, received: [$DB_TYPE]"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
re='^[0-9]+$'
|
||||
if ! [[ $DB_PORT =~ $re ]] ; then
|
||||
DB_PORT=3306
|
||||
fi
|
||||
|
||||
echo "Wait for MySQL DB connection ..."
|
||||
until php /dbtest.php $DB_HOST $DB_BASE $DB_PORT $DB_USER $DB_PASS; do
|
||||
echo Checking DB: $?
|
||||
sleep 3
|
||||
done
|
||||
echo "Connection established"
|
||||
}
|
||||
|
||||
function handleStartup() {
|
||||
# These are idempotent, run them anyway
|
||||
/opt/kimai/bin/console -n kimai:install
|
||||
/opt/kimai/bin/console -n kimai:update
|
||||
if [ ! -z "$ADMINPASS" ] && [ ! -a "$ADMINMAIL" ]; then
|
||||
/opt/kimai/bin/console kimai:user:create superadmin "$ADMINMAIL" ROLE_SUPER_ADMIN "$ADMINPASS"
|
||||
fi
|
||||
echo "$KIMAI" > /opt/kimai/var/installed
|
||||
echo "Kimai2 ready"
|
||||
}
|
||||
|
||||
function runServer() {
|
||||
# Just while I'm fixing things
|
||||
/opt/kimai/bin/console kimai:reload --env="$APP_ENV"
|
||||
chown -R $USER_ID:$GROUP_ID /opt/kimai/var
|
||||
if [ -e /use_apache ]; then
|
||||
exec /usr/sbin/apache2 -D FOREGROUND
|
||||
elif [ -e /use_fpm ]; then
|
||||
exec php-fpm
|
||||
else
|
||||
echo "Error, unknown server type"
|
||||
fi
|
||||
}
|
||||
|
||||
waitForDB
|
||||
handleStartup
|
||||
runServer
|
||||
@@ -1,23 +1,17 @@
|
||||
; top-most EditorConfig file
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
; Unix-style newlines
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
|
||||
[*.twig]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.yaml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
[{compose.yaml,compose.*.yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
61
.env.dist
61
.env.dist
@@ -1,7 +1,18 @@
|
||||
# In all environments, the following files are loaded if they exist,
|
||||
# the latter taking precedence over the former:
|
||||
#
|
||||
# You should NOT use this file in production, but instead move the environment variables to your webserver configuration.
|
||||
# The .env file is only existing to simplify the initial setup!
|
||||
# * .env contains default values for the environment variables needed by the app
|
||||
# * .env.local uncommitted file with local overrides
|
||||
# * .env.$APP_ENV committed environment-specific defaults
|
||||
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||
#
|
||||
# Real environment variables win over .env files.
|
||||
#
|
||||
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||
# https://symfony.com/doc/current/configuration/secrets.html
|
||||
#
|
||||
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||
|
||||
#================================================================================
|
||||
# Configure your database connection and set the correct server version.
|
||||
@@ -16,32 +27,34 @@
|
||||
# For MySQL that would be "serverVersion=5.7" as in:
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7
|
||||
#
|
||||
# For MariaDB it would be "serverVersion=10.5.8-MariaDB":
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
|
||||
# For MariaDB it would be "serverVersion=10.11.15-MariaDB":
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.11.15-MariaDB
|
||||
#
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.11.15-MariaDB
|
||||
|
||||
#================================================================================
|
||||
# The full documentation can be found at https://www.kimai.org/documentation/emails.html
|
||||
#
|
||||
# Email will be sent with this address as sender:
|
||||
# Running behind reverse proxies? Try these:
|
||||
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
|
||||
# TRUSTED_HOSTS=localhost|example.com
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=prod
|
||||
APP_SECRET=
|
||||
APP_SHARE_DIR=var/share
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
# Documentation at https://www.kimai.org/documentation/emails.html
|
||||
MAILER_FROM=kimai@example.com
|
||||
# Email connection (disabled by default) - see documentation for the format
|
||||
MAILER_URL=null://null
|
||||
###< symfony/mailer ###
|
||||
|
||||
#================================================================================
|
||||
# do not change, unless you are developing for Kimai
|
||||
APP_ENV=prod
|
||||
###> nelmio/cors-bundle ###
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
###< nelmio/cors-bundle ###
|
||||
|
||||
#================================================================================
|
||||
# should be changed to a unique character sequence, used for hashing cookies
|
||||
APP_SECRET=change_this_to_something_unique
|
||||
|
||||
#================================================================================
|
||||
# Running behind reverse proxies? Try these:
|
||||
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
|
||||
# TRUSTED_HOSTS=localhost,example.com
|
||||
|
||||
#================================================================================
|
||||
# unlikely, that you need to change this one
|
||||
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$
|
||||
###> symfony/routing ###
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
DEFAULT_URI=http://localhost
|
||||
###< symfony/routing ###
|
||||
|
||||
11
.eslintrc.js
11
.eslintrc.js
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"amd": true,
|
||||
},
|
||||
parser: '@babel/eslint-parser',
|
||||
extends: ['eslint:recommended'],
|
||||
ignorePatterns: ["assets/*.js"],
|
||||
}
|
||||
20
.gitattributes
vendored
Normal file
20
.gitattributes
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
.docker export-ignore
|
||||
.github export-ignore
|
||||
assets export-ignore
|
||||
tests export-ignore
|
||||
.codecov.yml export-ignore
|
||||
.editorconfig export-ignore
|
||||
eslint.config.js export-ignore
|
||||
eslint.config.mjs export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.php-cs-fixer.dist.php export-ignore
|
||||
php-cs-fixer.dist.php export-ignore
|
||||
babel.config.js export-ignore
|
||||
package.json export-ignore
|
||||
php-cs-fixer.sh export-ignore
|
||||
phpstan.neon export-ignore
|
||||
phpstan.sh export-ignore
|
||||
phpunit.xml.dist export-ignore
|
||||
webpack.config.js export-ignore
|
||||
yarn.lock export-ignore
|
||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,2 +1,3 @@
|
||||
github: [kevinpapst]
|
||||
custom: ["https://www.kimai.org/", "https://www.kimai.tw/", "https://www.kimai.co.il/"]
|
||||
open_collective: kimai
|
||||
custom: ["https://www.kimai.org/", "https://www.kimai.cloud/", "https://www.kevinpapst.de/"]
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -53,12 +53,12 @@ body:
|
||||
attributes:
|
||||
label: Which PHP version are you using?
|
||||
options:
|
||||
- "8.5"
|
||||
- "8.4"
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.1"
|
||||
- Unknown
|
||||
- "8.2"
|
||||
- "8.0"
|
||||
- "7.4"
|
||||
- "7.3"
|
||||
- Other (please mention below)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
7
.github/release-drafter.yml
vendored
7
.github/release-drafter.yml
vendored
@@ -7,6 +7,7 @@ exclude-labels:
|
||||
- 'release'
|
||||
exclude-contributors:
|
||||
- 'dependabot'
|
||||
- 'weblate'
|
||||
change-template: '- $TITLE (#$NUMBER)'
|
||||
change-title-escapes: '\<*_&`#@'
|
||||
version-resolver:
|
||||
@@ -21,10 +22,8 @@ version-resolver:
|
||||
- 'translation'
|
||||
default: patch
|
||||
template: |
|
||||
[Upgrade Kimai](https://www.kimai.org/documentation/updates.html) - [Install Kimai](https://www.kimai.org/documentation/installation.html) - [Docker](https://www.kimai.org/documentation/docker.html)
|
||||
|
||||
**Compatible with PHP 8.1 to 8.3**
|
||||
**Compatible with PHP 8.1 to 8.5**
|
||||
|
||||
$CHANGES
|
||||
|
||||
Involved in this release: $CONTRIBUTORS - thank you!
|
||||
Involved in this release: $CONTRIBUTORS
|
||||
|
||||
43
.github/workflows/docker-base.yaml
vendored
43
.github/workflows/docker-base.yaml
vendored
@@ -1,43 +0,0 @@
|
||||
name: 'Docker base image'
|
||||
# act -j build -W ./.github/workflows/docker-base.yaml --secret-file .dockerhub.secrets
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 4 5 * *" # build on the 5th of every month
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
server: [ fpm, apache ]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{secrets.DOCKERHUB_USERNAME}}
|
||||
password: ${{secrets.DOCKERHUB_PASSWORD}}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .docker
|
||||
file: .docker/Dockerfile.kimai-base
|
||||
build-args: |
|
||||
TIMEZONE=Europe/London
|
||||
BASE=${{ matrix.server }}
|
||||
platforms: linux/amd64,linux/arm64 #,linux/arm/v8,linux/arm/v7,linux/arm/v6
|
||||
tags: |
|
||||
kimai/kimai-base:${{ matrix.server }}
|
||||
push: true
|
||||
|
||||
|
||||
130
.github/workflows/docker.yaml
vendored
130
.github/workflows/docker.yaml
vendored
@@ -1,17 +1,16 @@
|
||||
name: 'Docker Build'
|
||||
# act -j build -W ./.github/workflows/docker.yaml --secret-file .dockerhub.secrets
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
kimai_tag:
|
||||
description: 'Kimai tag to build'
|
||||
required: true
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
type: [ dev, prod ]
|
||||
server: [ fpm, apache ]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -20,85 +19,74 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Install lastversion
|
||||
run: sudo apt-get update -y; sudo apt-get install --no-install-recommends -y python3 python3-pip; pip install lastversion
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{secrets.DOCKERHUB_USERNAME}}
|
||||
password: ${{secrets.DOCKERHUB_PASSWORD}}
|
||||
|
||||
- name: get lastest version
|
||||
id: remote_version
|
||||
run: echo "kimai_version=$(lastversion https://github.com/kimai/kimai)" >> $GITHUB_ENV
|
||||
- name: Determine version
|
||||
run: |
|
||||
input="${{ github.event.inputs.kimai_tag }}"
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v4
|
||||
# Determine between manual trigger and release event
|
||||
if [ -z "$input" ]; then
|
||||
echo "Using release tag: ${{ github.event.release.tag_name }}"
|
||||
version="${{ github.event.release.tag_name }}"
|
||||
else
|
||||
echo "Using tag provided: $input"
|
||||
version="$input"
|
||||
fi
|
||||
|
||||
if [[ ! $version =~ ^2\.(0|[1-9]*)(0?)\.(0|[0-9]*)(0?)$ ]]; then
|
||||
echo "Invalid version number: $version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "kimai_version=$version" >> $GITHUB_ENV
|
||||
|
||||
- name: FPM image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
build-args: |
|
||||
KIMAI=${{ env.kimai_version }}
|
||||
TIMEZONE=Europe/London
|
||||
BASE=${{ matrix.server }}
|
||||
target: ${{ matrix.type }}
|
||||
platforms: linux/amd64,linux/arm64 #,linux/arm/v8,linux/arm/v7,linux/arm/v6
|
||||
BASE=fpm
|
||||
target: prod
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
kimai/kimai2:${{ matrix.server }}-${{ matrix.type }}
|
||||
kimai/kimai2:${{ matrix.server }}-${{ env.kimai_version }}-${{ matrix.type }}
|
||||
kimai/kimai2:latest
|
||||
kimai/kimai2:fpm
|
||||
push: true
|
||||
|
||||
tag:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
- name: Apache image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
username: ${{secrets.DOCKERHUB_USERNAME}}
|
||||
password: ${{secrets.DOCKERHUB_PASSWORD}}
|
||||
context: .
|
||||
file: Dockerfile
|
||||
build-args: |
|
||||
KIMAI=${{ env.kimai_version }}
|
||||
BASE=apache
|
||||
target: prod
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
kimai/kimai2:apache
|
||||
kimai/kimai2:apache-${{ env.kimai_version }}
|
||||
push: true
|
||||
|
||||
- name: Pull images
|
||||
run: |
|
||||
docker pull kimai/kimai2:fpm-prod
|
||||
docker pull kimai/kimai2:fpm-dev
|
||||
docker pull kimai/kimai2:apache-prod
|
||||
docker pull kimai/kimai2:apache-dev
|
||||
|
||||
- name: Tag fpm
|
||||
run: |
|
||||
docker tag kimai/kimai2:fpm-prod kimai/kimai2:fpm
|
||||
docker push kimai/kimai2:fpm
|
||||
|
||||
- name: Tag fpm latest
|
||||
run: |
|
||||
docker tag kimai/kimai2:fpm-prod kimai/kimai2:fpm-latest
|
||||
docker push kimai/kimai2:fpm-latest
|
||||
|
||||
- name: Tag latest
|
||||
run: |
|
||||
docker tag kimai/kimai2:fpm-prod kimai/kimai2:latest
|
||||
docker push kimai/kimai2:latest
|
||||
|
||||
- name: Tag prod
|
||||
run: |
|
||||
docker tag kimai/kimai2:fpm-prod kimai/kimai2:prod
|
||||
docker push kimai/kimai2:prod
|
||||
|
||||
- name: Tag apache
|
||||
run: |
|
||||
docker tag kimai/kimai2:apache-prod kimai/kimai2:apache
|
||||
docker push kimai/kimai2:apache
|
||||
|
||||
- name: Tag apache latest
|
||||
run: |
|
||||
docker tag kimai/kimai2:apache-prod kimai/kimai2:apache-latest
|
||||
docker push kimai/kimai2:apache-latest
|
||||
|
||||
- name: Tag dev
|
||||
run: |
|
||||
docker tag kimai/kimai2:apache-dev kimai/kimai2:dev
|
||||
docker push kimai/kimai2:dev
|
||||
- name: Development image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
build-args: |
|
||||
KIMAI=${{ env.kimai_version }}
|
||||
BASE=apache
|
||||
target: dev
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
kimai/kimai2:dev
|
||||
push: true
|
||||
|
||||
16
.github/workflows/lockfiles.yaml
vendored
16
.github/workflows/lockfiles.yaml
vendored
@@ -1,23 +1,21 @@
|
||||
name: Check .lock files
|
||||
on:
|
||||
pull_request: null
|
||||
pull_request_target: null
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
lockfiles:
|
||||
runs-on: ubuntu-latest
|
||||
name: Verify lock file integrity
|
||||
steps:
|
||||
|
||||
- name: Clone Kimai
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prevent file change
|
||||
uses: xalvarez/prevent-file-change-action@v1
|
||||
uses: xalvarez/prevent-file-change-action@v3
|
||||
with:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
pattern: .*\.lock$
|
||||
pattern: .*\.lock$|^\.github\/.*$
|
||||
trustedAuthors: kevinpapst, dependabot
|
||||
|
||||
14
.github/workflows/testing.yaml
vendored
14
.github/workflows/testing.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.1', '8.2', '8.3']
|
||||
php: ['8.4', '8.5']
|
||||
|
||||
name: Integration (${{ matrix.php }})
|
||||
steps:
|
||||
@@ -56,13 +56,13 @@ jobs:
|
||||
run: composer install
|
||||
|
||||
- name: Validate Composer
|
||||
run: composer validate --strict
|
||||
run: composer validate --strict --no-check-all
|
||||
|
||||
- name: Warmup cache
|
||||
run: APP_ENV=dev bin/console kimai:reload -n
|
||||
|
||||
- name: Check codestyles
|
||||
run: PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php-cs-fixer.dist.php --using-cache=no --show-progress=none --format=checkstyle | cs2pr
|
||||
run: vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php-cs-fixer.dist.php --using-cache=no --show-progress=none --format=checkstyle | cs2pr
|
||||
|
||||
- name: Run PHPStan for application
|
||||
run: vendor/bin/phpstan analyse -c phpstan.neon --no-progress --error-format=checkstyle | cs2pr
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
MAILER_URL: null://localhost
|
||||
|
||||
- name: Full test-suite
|
||||
if: matrix.php != '8.2'
|
||||
if: matrix.php != '8.5'
|
||||
run: vendor/bin/phpunit tests/
|
||||
env:
|
||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
MAILER_URL: null://localhost
|
||||
|
||||
- name: Full test-suite with coverage
|
||||
if: matrix.php == '8.2'
|
||||
if: matrix.php == '8.5'
|
||||
run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml
|
||||
env:
|
||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
|
||||
@@ -103,8 +103,8 @@ jobs:
|
||||
MAILER_URL: null://localhost
|
||||
|
||||
- name: Upload code coverage
|
||||
if: matrix.php == '8.2'
|
||||
uses: codecov/codecov-action@v4
|
||||
if: matrix.php == '8.5'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage.xml
|
||||
|
||||
2
.github/workflows/website.yaml
vendored
2
.github/workflows/website.yaml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
echo "Using input provided: $input"
|
||||
version="$input"
|
||||
fi
|
||||
|
||||
|
||||
echo "kimai_version=$version" >> $GITHUB_ENV
|
||||
|
||||
if [[ ! $version =~ ^2\.(0|[1-9]*)(0?)\.(0|[0-9]*)(0?)$ ]]; then
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,9 +1,9 @@
|
||||
# some hosters require a htaccess to change the PHP version
|
||||
/public/.htaccess
|
||||
/.env-*
|
||||
/.idea/
|
||||
.DS_Store
|
||||
/phpstan.sh
|
||||
var/templates/
|
||||
|
||||
|
||||
# custom apache rules e.g. to deactivate ioncube loader
|
||||
/public/.user.ini
|
||||
@@ -15,12 +15,15 @@
|
||||
# YARN 2
|
||||
/.yarnrc.yml
|
||||
/.yarn
|
||||
/.pnp.*
|
||||
|
||||
# for keeping empty directories
|
||||
/config/packages/local.yaml
|
||||
/config/bundles-local.php
|
||||
/var/dev/*
|
||||
/var/data/*
|
||||
/var/cache/*
|
||||
/var/share/*
|
||||
/var/invoices*
|
||||
/var/export*
|
||||
/var/log/*
|
||||
@@ -34,7 +37,6 @@
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
.env
|
||||
/public/bundles/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
@@ -45,9 +47,8 @@
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
.php_cs
|
||||
.php_cs.cache
|
||||
.php-cs-fixer.cache
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
|
||||
@@ -8,6 +8,7 @@ file that was distributed with this source code.
|
||||
COMMENT;
|
||||
|
||||
$fixer = new PhpCsFixer\Config();
|
||||
$fixer->setUnsupportedPhpVersionAllowed(true);
|
||||
$fixer
|
||||
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
|
||||
->setRiskyAllowed(true)
|
||||
@@ -43,6 +44,7 @@ $fixer
|
||||
'single_line_after_imports' => true,
|
||||
'switch_case_semicolon_to_colon' => true,
|
||||
'switch_case_space' => true,
|
||||
'php_unit_method_casing' => true,
|
||||
'array_syntax' => [
|
||||
'syntax' => 'short'
|
||||
],
|
||||
@@ -141,7 +143,7 @@ $fixer
|
||||
'whitespace_after_comma_in_array' => true,
|
||||
'yoda_style' => false,
|
||||
'ternary_to_null_coalescing' => true,
|
||||
'visibility_required' => ['elements' => [
|
||||
'modifier_keywords' => ['elements' => [
|
||||
'const',
|
||||
'method',
|
||||
'property',
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# Contributing
|
||||
|
||||
Kimai is an open source project, contributions made by the community are welcome.
|
||||
Kimai is an open source project, contributions made by the community are welcome.
|
||||
But we can only accept contributions with a signed CLA (Contributor License Agreement) to prevent issues in the future (you will see a link when opening a PR).
|
||||
|
||||
Send your ideas, code reviews, pull requests and feature requests to help to improve this project.
|
||||
|
||||
## Pull request rules
|
||||
|
||||
- Make your changes in a new git branch, based on the latest code in `main`
|
||||
- Apply our code-style by running `composer codestyle-fix`
|
||||
- Run the static code analysis with `composer phpstan`
|
||||
- Verify everything still works with `composer tests-unit` and `composer tests-integration`
|
||||
- Verify everything still works with `composer tests`
|
||||
- Add tests for your changes
|
||||
- When sending in a PR, you must accept that your contributions/code will be published under MIT license (see the [LICENSE](LICENSE) file as well), otherwise your PR will be closed
|
||||
- If one of the PR checks/builds fails, fix it before asking for a review
|
||||
|
||||
Further documentation can be found in the [developer documentation](https://www.kimai.org/documentation/developers.html).
|
||||
|
||||
278
Dockerfile
278
Dockerfile
@@ -4,12 +4,27 @@
|
||||
# | . \| | | | | | | (_| | |
|
||||
# |_|\_\_|_| |_| |_|\__,_|_|
|
||||
#
|
||||
# Kimai images for:
|
||||
# - plain PHP FPM (kimai/kimai2:fpm)
|
||||
# - Apache with PHP (kimai/kimai2:apache)
|
||||
# - Development (kimai/kimai2:dev)
|
||||
# ---------------------------------------------------------------------
|
||||
# For local testing by maintainer:
|
||||
#
|
||||
# docker build --no-cache -t kimai-fpm --build-arg BASE=fpm .
|
||||
# docker build --no-cache -t kimai-apache --build-arg BASE=apache .
|
||||
# docker run -d --name kimai-apache-app kimai-apache
|
||||
# docker exec -ti kimai-apache-app /bin/bash
|
||||
# ---------------------------------------------------------------------
|
||||
# Official PHP images: https://hub.docker.com/_/php/
|
||||
# https://github.com/docker-library/docs/blob/master/php/README.md#supported-tags-and-respective-dockerfile-links
|
||||
# Pass-through Arguments: https://benkyriakou.com/posts/docker-args-empty
|
||||
# Best practices: https://docs.docker.com/build/building/best-practices/
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# Source base [fpm/apache]
|
||||
# Source base, one of: fpm, apache
|
||||
ARG BASE="fpm"
|
||||
ARG PHP_VER="8.2"
|
||||
ARG COMPOSER_VER="latest"
|
||||
# Branch name
|
||||
# Kimai branch/tag to run
|
||||
ARG KIMAI="main"
|
||||
# Timezone for images
|
||||
ARG TIMEZONE="Europe/Berlin"
|
||||
@@ -18,36 +33,221 @@ ARG TIMEZONE="Europe/Berlin"
|
||||
# Shared tools
|
||||
###########################
|
||||
|
||||
# full kimai source
|
||||
FROM alpine:latest AS git-dev
|
||||
# pass-through Arguments in every stage. See: https://benkyriakou.com/posts/docker-args-empty
|
||||
ARG KIMAI
|
||||
# composer base image
|
||||
FROM composer:latest AS composer
|
||||
|
||||
###########################
|
||||
# PHP extensions
|
||||
###########################
|
||||
|
||||
# fpm alpine php extension base
|
||||
FROM php:8.3-fpm-alpine AS fpm-php-ext-base
|
||||
RUN apk add --no-cache \
|
||||
# build-tools
|
||||
autoconf \
|
||||
dpkg \
|
||||
dpkg-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
icu-dev \
|
||||
libatomic \
|
||||
libc-dev \
|
||||
libgomp \
|
||||
libmagic \
|
||||
m4 \
|
||||
make \
|
||||
mpc1 \
|
||||
mpfr4 \
|
||||
musl-dev \
|
||||
perl \
|
||||
re2c \
|
||||
# gd
|
||||
freetype-dev \
|
||||
libpng-dev \
|
||||
# icu
|
||||
icu-dev \
|
||||
icu-data-full \
|
||||
# ldap
|
||||
openldap-dev \
|
||||
libldap \
|
||||
# zip
|
||||
libzip-dev \
|
||||
# xsl
|
||||
libxslt-dev
|
||||
|
||||
# apache debian php extension base
|
||||
FROM php:8.3-apache-bookworm AS apache-php-ext-base
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
libldap2-dev \
|
||||
libicu-dev \
|
||||
libpng-dev \
|
||||
libzip-dev \
|
||||
libxslt1-dev \
|
||||
libfreetype6-dev
|
||||
|
||||
# php extension gd - 13.86s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-gd
|
||||
RUN docker-php-ext-configure gd \
|
||||
--with-freetype && \
|
||||
docker-php-ext-install -j$(nproc) gd
|
||||
|
||||
# php extension intl : 15.26s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-intl
|
||||
RUN docker-php-ext-install -j$(nproc) intl
|
||||
|
||||
# php extension ldap : 8.45s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-ldap
|
||||
RUN docker-php-ext-configure ldap && \
|
||||
docker-php-ext-install -j$(nproc) ldap
|
||||
|
||||
# php extension pdo_mysql : 6.14s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-pdo_mysql
|
||||
RUN docker-php-ext-install -j$(nproc) pdo_mysql
|
||||
|
||||
# php extension zip : 8.18s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-zip
|
||||
RUN docker-php-ext-install -j$(nproc) zip
|
||||
|
||||
# php extension xsl : ?.?? s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-xsl
|
||||
RUN docker-php-ext-install -j$(nproc) xsl
|
||||
|
||||
# php extension opcache
|
||||
FROM ${BASE}-php-ext-base AS php-ext-opcache
|
||||
RUN docker-php-ext-install -j$(nproc) opcache
|
||||
|
||||
###########################
|
||||
# fpm base build
|
||||
###########################
|
||||
|
||||
FROM php:8.3-fpm-alpine AS fpm-base
|
||||
ARG TIMEZONE
|
||||
RUN apk add --no-cache git && \
|
||||
git clone --depth 1 --branch ${KIMAI} https://github.com/kimai/kimai.git /opt/kimai
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
coreutils \
|
||||
freetype \
|
||||
haveged \
|
||||
icu \
|
||||
icu-data-full \
|
||||
libldap \
|
||||
libpng \
|
||||
libzip \
|
||||
libxslt-dev \
|
||||
fcgi \
|
||||
tzdata && \
|
||||
touch /use_fpm && \
|
||||
sed -i "s/;ping.path/ping.path/g" /usr/local/etc/php-fpm.d/www.conf && \
|
||||
sed -i "s/;access.suppress_path\[\] = \/ping/access.suppress_path\[\] = \/ping/g" /usr/local/etc/php-fpm.d/www.conf
|
||||
|
||||
# production kimai source
|
||||
FROM git-dev AS git-prod
|
||||
WORKDIR /opt/kimai
|
||||
RUN rm -r tests
|
||||
EXPOSE 9000
|
||||
|
||||
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
||||
CMD \
|
||||
SCRIPT_NAME=/ping \
|
||||
SCRIPT_FILENAME=/ping \
|
||||
REQUEST_METHOD=GET \
|
||||
cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1
|
||||
|
||||
# FPM base
|
||||
FROM kimai/kimai-base:fpm as fpm-base
|
||||
###########################
|
||||
# apache base build
|
||||
###########################
|
||||
|
||||
FROM php:8.3-apache-bookworm AS apache-base
|
||||
ARG TIMEZONE
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
bash \
|
||||
haveged \
|
||||
libicu72 \
|
||||
libldap-common \
|
||||
libpng16-16 \
|
||||
libzip4 \
|
||||
libxslt1.1 \
|
||||
libfreetype6 \
|
||||
unzip && \
|
||||
echo "Listen 8001" > /etc/apache2/ports.conf && \
|
||||
a2enmod rewrite && \
|
||||
touch /use_apache
|
||||
|
||||
# Apache base
|
||||
FROM kimai/kimai-base:apache as apache-base
|
||||
COPY .docker/000-default.conf /etc/apache2/sites-available/000-default.conf
|
||||
|
||||
EXPOSE 8001
|
||||
|
||||
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
||||
CMD curl -f http://127.0.0.1:8001 || exit 1
|
||||
|
||||
###########################
|
||||
# global base build
|
||||
###########################
|
||||
|
||||
FROM ${BASE}-base AS base
|
||||
FROM ${BASE}-base AS php-base
|
||||
ARG TIMEZONE
|
||||
|
||||
ENV TIMEZONE=${TIMEZONE}
|
||||
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone && \
|
||||
# make composer home dir
|
||||
mkdir /composer && \
|
||||
chown -R www-data:www-data /composer
|
||||
|
||||
# copy composer
|
||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# copy php extensions
|
||||
|
||||
# PHP extension xsl
|
||||
COPY --from=php-ext-xsl /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini
|
||||
COPY --from=php-ext-xsl /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xsl.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xsl.so
|
||||
# PHP extension pdo_mysql
|
||||
COPY --from=php-ext-pdo_mysql /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini
|
||||
COPY --from=php-ext-pdo_mysql /usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so
|
||||
# PHP extension zip
|
||||
COPY --from=php-ext-zip /usr/local/etc/php/conf.d/docker-php-ext-zip.ini /usr/local/etc/php/conf.d/docker-php-ext-zip.ini
|
||||
COPY --from=php-ext-zip /usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so
|
||||
# PHP extension ldap
|
||||
COPY --from=php-ext-ldap /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini
|
||||
COPY --from=php-ext-ldap /usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so
|
||||
# PHP extension gd
|
||||
COPY --from=php-ext-gd /usr/local/etc/php/conf.d/docker-php-ext-gd.ini /usr/local/etc/php/conf.d/docker-php-ext-gd.ini
|
||||
COPY --from=php-ext-gd /usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so
|
||||
# PHP extension intl
|
||||
COPY --from=php-ext-intl /usr/local/etc/php/conf.d/docker-php-ext-intl.ini /usr/local/etc/php/conf.d/docker-php-ext-intl.ini
|
||||
COPY --from=php-ext-intl /usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so
|
||||
# PHP extension opcache
|
||||
COPY --from=php-ext-opcache /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
|
||||
|
||||
###########################
|
||||
# fetch Kimai sources
|
||||
###########################
|
||||
|
||||
FROM alpine:latest AS git-prod
|
||||
ARG KIMAI
|
||||
ARG TIMEZONE
|
||||
# the convention in the Kimai repository is: tags are always version numbers, branch names always start with a letter
|
||||
# if the KIMAI variable starts with a number (e.g. 2.24.0) we assume its a tag, otherwise its a branch
|
||||
RUN [[ $KIMAI =~ ^[0-9] ]] && export REF='tags' || export REF='heads' && \
|
||||
wget -O "/opt/kimai.tar.gz" "https://github.com/kimai/kimai/archive/refs/${REF}/${KIMAI}.tar.gz" && \
|
||||
tar -xpzf /opt/kimai.tar.gz -C /opt/ && \
|
||||
mv /opt/kimai-${KIMAI} /opt/kimai
|
||||
|
||||
###########################
|
||||
# global base build
|
||||
###########################
|
||||
|
||||
FROM php-base AS base
|
||||
ARG KIMAI
|
||||
ARG TIMEZONE
|
||||
|
||||
LABEL maintainer="tobias@neontribe.co.uk"
|
||||
LABEL org.opencontainers.image.title="Kimai" \
|
||||
org.opencontainers.image.description="Kimai is a time-tracking application." \
|
||||
org.opencontainers.image.authors="Kimai Community" \
|
||||
org.opencontainers.image.url="https://www.kimai.org/" \
|
||||
org.opencontainers.image.documentation="https://www.kimai.org/documentation/" \
|
||||
org.opencontainers.image.source="https://github.com/kimai/kimai" \
|
||||
org.opencontainers.image.version="${KIMAI}" \
|
||||
org.opencontainers.image.vendor="Kevin Papst" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0"
|
||||
|
||||
ENV KIMAI=${KIMAI}
|
||||
ENV TIMEZONE=${TIMEZONE}
|
||||
@@ -56,49 +256,37 @@ RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} >
|
||||
chown -R www-data:www-data /composer
|
||||
|
||||
# copy startup script & DB checking script
|
||||
COPY .docker/startup.sh /startup.sh
|
||||
COPY .docker/service.sh /service.sh
|
||||
COPY .docker/self-test.sh /self-test.sh
|
||||
COPY .docker/dbtest.php /dbtest.php
|
||||
COPY .docker/entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENV DATABASE_URL="mysql://kimai:kimai@127.0.0.1:3306/kimai?charset=utf8mb4&serverVersion=5.7.40"
|
||||
ENV DATABASE_URL="mysql://kimai:kimai@127.0.0.1:3306/kimai?charset=utf8mb4&serverVersion=8.3"
|
||||
ENV APP_SECRET=change_this_to_something_unique
|
||||
# The default container name for nginx is nginx
|
||||
ENV TRUSTED_PROXIES=nginx,localhost,127.0.0.1
|
||||
ENV TRUSTED_HOSTS=nginx,localhost,127.0.0.1
|
||||
ENV MAILER_FROM=kimai@example.com
|
||||
ENV MAILER_URL=null://localhost
|
||||
ENV ADMINPASS=
|
||||
ENV ADMINMAIL=
|
||||
ENV DB_TYPE=
|
||||
ENV DB_USER=
|
||||
ENV DB_PASS=
|
||||
ENV DB_HOST=
|
||||
ENV DB_PORT=
|
||||
ENV DB_BASE=
|
||||
ENV COMPOSER_MEMORY_LIMIT=-1
|
||||
# If this set then the image will start, run a self test and then exit. It's used for the release process
|
||||
ENV TEST_AND_EXIT=
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
ENV USER_ID=
|
||||
ENV GROUP_ID=
|
||||
# default values to configure composer behavior
|
||||
ENV COMPOSER_MEMORY_LIMIT=-1
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
VOLUME [ "/opt/kimai/var" ]
|
||||
|
||||
CMD [ "/startup.sh" ]
|
||||
|
||||
CMD [ "/entrypoint.sh" ]
|
||||
|
||||
###########################
|
||||
# final builds
|
||||
###########################
|
||||
|
||||
# developement build
|
||||
# development build
|
||||
FROM base AS dev
|
||||
# copy kimai develop source
|
||||
COPY --from=git-dev --chown=www-data:www-data /opt/kimai /opt/kimai
|
||||
COPY --from=git-prod --chown=www-data:www-data /opt/kimai /opt/kimai
|
||||
COPY .docker /assets
|
||||
# do the composer deps installation
|
||||
RUN echo \$PATH
|
||||
RUN \
|
||||
export COMPOSER_HOME=/composer && \
|
||||
composer --no-ansi install --working-dir=/opt/kimai --optimize-autoloader && \
|
||||
@@ -109,13 +297,12 @@ RUN \
|
||||
mkdir -p /opt/kimai/var/logs && chmod 777 /opt/kimai/var/logs && \
|
||||
sed "s/128M/-1/g" /usr/local/etc/php/php.ini-development > /opt/kimai/php-cli.ini && \
|
||||
sed -i "s/env php/env -S php -c \/opt\/kimai\/php-cli.ini/g" /opt/kimai/bin/console && \
|
||||
tar -C /opt/kimai -zcvf /var/tmp/public.tgz public && \
|
||||
/opt/kimai/bin/console kimai:version | awk '{print $2}' > /opt/kimai/version.txt
|
||||
ENV APP_ENV=dev
|
||||
ENV DATABASE_URL=
|
||||
ENV memory_limit=256M
|
||||
ENV memory_limit=512M
|
||||
|
||||
# production build
|
||||
# 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
|
||||
# copy kimai production source
|
||||
COPY --from=git-prod --chown=www-data:www-data /opt/kimai /opt/kimai
|
||||
@@ -125,7 +312,7 @@ RUN \
|
||||
export COMPOSER_HOME=/composer && \
|
||||
composer --no-ansi install --working-dir=/opt/kimai --no-dev --optimize-autoloader && \
|
||||
composer --no-ansi clearcache && \
|
||||
composer --no-ansi require --working-dir=/opt/kimai laminas/laminas-ldap && \
|
||||
composer --no-ansi require --update-no-dev --working-dir=/opt/kimai laminas/laminas-ldap && \
|
||||
cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini && \
|
||||
sed -i "s/expose_php = On/expose_php = Off/g" /usr/local/etc/php/php.ini && \
|
||||
sed -i "s/;opcache.enable=1/opcache.enable=1/g" /usr/local/etc/php/php.ini && \
|
||||
@@ -137,8 +324,7 @@ RUN \
|
||||
mkdir -p /opt/kimai/var/logs && chmod 777 /opt/kimai/var/logs && \
|
||||
sed "s/128M/-1/g" /usr/local/etc/php/php.ini-development > /opt/kimai/php-cli.ini && \
|
||||
chown -R www-data:www-data /opt/kimai /usr/local/etc/php/php.ini && \
|
||||
tar -C /opt/kimai -zcvf /var/tmp/public.tgz public && \
|
||||
/opt/kimai/bin/console kimai:version | awk '{print $2}' > /opt/kimai/version.txt
|
||||
ENV APP_ENV=prod
|
||||
ENV DATABASE_URL=
|
||||
ENV memory_limit=256M
|
||||
ENV memory_limit=512M
|
||||
|
||||
33
README.md
33
README.md
@@ -21,15 +21,6 @@ authentication via SAML/LDAP/Database, two-factor authentication (2FA) with TOTP
|
||||
user/customer/project specific rates, advanced search & filtering, money and time budgets, advanced reporting, support for [plugins](https://www.kimai.org/store/)
|
||||
and so much more.
|
||||
|
||||
### Versions
|
||||
|
||||
There are two [versions](https://www.kimai.org/documentation/versions.html) of Kimai existing:
|
||||
|
||||
- [Version 2](https://github.com/kimai/kimai) — the current stable release (PHP 8.1+)
|
||||
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — EOL since mid of 2023 (PHP 7.4)
|
||||
|
||||
Do **NOT** use Version 1, it won't get any more updates!
|
||||
|
||||
### Links
|
||||
|
||||
- [Home](https://www.kimai.org) — Kimai project homepage
|
||||
@@ -38,25 +29,22 @@ Do **NOT** use Version 1, it won't get any more updates!
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 8.1.3 minimum
|
||||
- PHP 8.1.3 minimum with support for 8.2, 8.3, 8.4, 8.5
|
||||
- MariaDB or MySQL
|
||||
- A webserver and subdomain (subdirectory is not supported)
|
||||
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip`
|
||||
|
||||
Support for PHP 8.2 and 8.3.
|
||||
|
||||
## Installation
|
||||
|
||||
- [Native setup](https://www.kimai.org/documentation/installation.html) — with Git and Composer
|
||||
- [Docker](https://hub.docker.com/r/kimai/kimai2) — containerized by @tobybatch
|
||||
- Caddy with Docker-Compose at [Hetzner](https://www.kimai.org/documentation/hosting-hetzner-cloud.html) and [DigitalOcean](https://www.kimai.org/documentation/hosting-digital-ocean.html)
|
||||
- [SSH setup](https://www.kimai.org/documentation/installation.html) with Git and Composer
|
||||
- [Docker images](https://hub.docker.com/r/kimai/kimai2) with FPM only or incl. Apache
|
||||
- [Synology](https://www.kimai.org/documentation/synology.html) user can host the Docker version
|
||||
- [Developer setups](https://www.kimai.org/documentation/developers.html) if you want to create Kimai integrations
|
||||
|
||||
There are also documentations for:
|
||||
- [developer setups](https://www.kimai.org/documentation/developers.html) — on your local machine
|
||||
- [shared hostings](https://www.kimai.org/documentation/installation.html#shared-hosting) — the least favorable option
|
||||
- [Synology](https://www.kimai.org/documentation/synology.html) — you could try to host the Docker version instead
|
||||
- [1-click installer](https://www.kimai.org/documentation/installation.html#hosting-and-1-click-installations) — hosted environments
|
||||
There are more documented ways for [on-premise hosting](https://www.kimai.org/documentation/chapter-on-premise.html).
|
||||
|
||||
And if you don't want to host Kimai, you can use [the Cloud version](https://www.kimai.cloud/) of it.
|
||||
And if you don't want to host Kimai, you can use the [Cloud version](https://www.kimai.cloud/) of it.
|
||||
|
||||
### Updating Kimai
|
||||
|
||||
@@ -65,13 +53,12 @@ And if you don't want to host Kimai, you can use [the Cloud version](https://www
|
||||
|
||||
### Plugins
|
||||
|
||||
- [Plugin marketplace](https://www.kimai.org/store/) — find existing plugins here
|
||||
- [Plugins](https://www.kimai.org/store/) — paid and free plugin marketplace
|
||||
- [Developer documentation](https://www.kimai.org/documentation/developers.html) — how to create a plugin
|
||||
|
||||
## Roadmap and releases
|
||||
|
||||
You can see a rough development roadmap in the [Milestones](https://github.com/kimai/kimai/milestones) sections.
|
||||
It is open for changes and input from the community, your [ideas and questions](https://github.com/kimai/kimai/issues) are welcome.
|
||||
You can see a rough development [roadmap](https://github.com/orgs/kimai/projects/2), which is open for changes and input from the community, your [ideas](https://github.com/kimai/kimai/issues) are welcome.
|
||||
|
||||
Release versions will be created on a regular basis, every couple of weeks latest.
|
||||
Every code change, whether it's a new feature or a bugfix, will be done on the `main` branch.
|
||||
|
||||
21
SECURITY.md
21
SECURITY.md
@@ -1,27 +1,10 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As announced in the [README](README.md) I only support the latest available release and `main` branch.
|
||||
As announced in the [README](README.md) security fixes will only be added to the `main` branch.
|
||||
|
||||
| Version | Supported |
|
||||
|----------------------|--------------------|
|
||||
| main branch | :white_check_mark: |
|
||||
| latest minor release | :white_check_mark: |
|
||||
| older releases | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please read the [Bughunter](https://www.kimai.org/documentation/bughunter.html) documentation before posting.
|
||||
You can report any security related vulnerability in the [advisory section at GitHub](https://github.com/kimai/kimai/security/advisories)
|
||||
or via email to [support@kimai.org](mailto:support@kimai.org).
|
||||
|
||||
I will work as fast as I can to fix the problem and publish a bugfix release / security update.
|
||||
Depending on the size of the required fixes, this might take a couple of hours or a couple of days.
|
||||
|
||||
You can expect that your message will be answered ASAP.
|
||||
|
||||
You will be mentioned in the release notes if your issue is valid.
|
||||
I am grateful for any (discrete) disclosure of vulnerabilities!
|
||||
|
||||
Please note: if you are asking for money, I will not reply. I receive these scam messages every day...
|
||||
You find all information in our [Bughunter documentation](https://www.kimai.org/documentation/bughunter.html).
|
||||
|
||||
50
UPGRADING-2.md
Normal file
50
UPGRADING-2.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Upgrading Kimai - Version 2.x
|
||||
|
||||
_Make sure to create a backup before you start!_
|
||||
|
||||
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how
|
||||
you can upgrade your Kimai installation to the latest stable release.
|
||||
|
||||
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
||||
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||
|
||||
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
|
||||
|
||||
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
|
||||
|
||||
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
|
||||
|
||||
**!! This release requires minimum PHP version to 8.1 !!**
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- All plugins need to be updated: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
|
||||
- The `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
|
||||
- removed: configuring the `dashboard` is not supported any longer
|
||||
- removed: custom translation files via `theme.branding.translation`
|
||||
- removed: changing the plugin directory via `kimai.plugin_dir`
|
||||
|
||||
### Developer
|
||||
|
||||
Developer read the full documentation at [https://www.kimai.org/documentation/migration-v2.html](https://www.kimai.org/documentation/migration-v2.html).
|
||||
|
||||
- Invoice renderer and templates for XML, JSON and TEXT were moved to the [Extended invoicing plugin](https://www.kimai.org/store/invoice-bundle.html) (install if you use one of those)
|
||||
- Moved `company.docx` to [external repo](https://github.com/kimai/invoice-templates/tree/main/docx-company) (needs to be re-uploaded if you want to keep on using it!)
|
||||
- Role names are forced to be uppercase
|
||||
- Removed unused `public/avatars/` directory
|
||||
- Time-tracking mode `duration_only` was removed, existing installations will be switched to `duration_fixed_begin`
|
||||
- Removed Twig filters. You might have to replace them in your custom export/invoice templates:
|
||||
- `date_full` => `date_time`
|
||||
- `duration_decimal` => `duration(true)`
|
||||
- `currency` => `currency_name`
|
||||
- `country` => `country_name`
|
||||
- `language` => `language_name`
|
||||
- Removed support for custom translation files (use [TranslationBundle](https://www.kimai.org/store/translation-bundle.html) instead or write your own plugin)
|
||||
- Removed all 3rd party mailer packages, you need to install them manually (ONLY if you used a short syntax to configure the `MAILER_URL` in `.env`):
|
||||
- `composer require symfony/amazon-mailer`
|
||||
- `composer require symfony/google-mailer`
|
||||
- `composer require symfony/mailchimp-mailer`
|
||||
- `composer require symfony/mailgun-mailer`
|
||||
- `composer require symfony/postmark-mailer`
|
||||
- `composer require symfony/sendgrid-mailer`
|
||||
37
UPGRADING-3.md
Normal file
37
UPGRADING-3.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Upgrading Kimai - Version 3.x
|
||||
|
||||
_Make sure to create a backup before you start!_
|
||||
|
||||
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how you can upgrade your Kimai installation to the latest stable release.
|
||||
|
||||
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
||||
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||
|
||||
## 3.0
|
||||
|
||||
**!! This release requires minimum PHP version 8.4 !!**
|
||||
|
||||
### Rename .env
|
||||
|
||||
Rename your file `.env` to `.env.local` or even better: move all variables to your webserver/container environment.
|
||||
|
||||
### Developer
|
||||
|
||||
Do not use method chaining: all fluent interface, especially in Entities, are no longer supported.
|
||||
|
||||
- Require PHP 8.4
|
||||
- Bump to Symfony 7.4
|
||||
- Removed old API token `X-AUTH-USER` and `X-AUTH-TOKEN`
|
||||
- Removed `TimesheetConstraint` - use a normal `Constraint` as base class and attach the `#[App\Validator\Attribute\TimesheetConstraint]` attribute
|
||||
- Removed `ProjectConstraint` - use a `FormExtension` and attach your custom constraints
|
||||
- Interface `MetaTableTypeInterface` has new methods: `getSection()`, `setSection()`
|
||||
- Interface `ExportRendererInterface` has new methods: `getType()`, `isInternal()`
|
||||
- Interface `ExportableItem` has new methods: `getTags()`, `getBreak()`
|
||||
- Removed and renamed translations, most important `action.edit` => `edit`, `my.profile` => `user_profile`
|
||||
- Removed `User::isExportDecimal()`
|
||||
- Use duration format `HH:mm` in default PDF exports
|
||||
- Replace Twig `AppVariable` with custom implementation
|
||||
- You need to adjust your templates if you access anything else then `app.locale`, `app.user`. `app.current_route`.
|
||||
- Most often used:
|
||||
- Replace `app.request.locale` with `app.locale`
|
||||
- Replace `app.request.attributes.get('_route')` with `app.current_route`
|
||||
51
UPGRADING.md
51
UPGRADING.md
@@ -1,50 +1,3 @@
|
||||
# Upgrading Kimai - Version2.x
|
||||
# Upgrading Kimai
|
||||
|
||||
_Make sure to create a backup before you start!_
|
||||
|
||||
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how
|
||||
you can upgrade your Kimai installation to the latest stable release.
|
||||
|
||||
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
||||
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||
|
||||
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
|
||||
|
||||
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
|
||||
|
||||
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
|
||||
|
||||
**!! This release requires minimum PHP version to 8.1 !!**
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- All plugins need to be updated: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
|
||||
- The `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
|
||||
- removed: configuring the `dashboard` is not supported any longer
|
||||
- removed: custom translation files via `theme.branding.translation`
|
||||
- removed: changing the plugin directory via `kimai.plugin_dir`
|
||||
|
||||
### Developer
|
||||
|
||||
Developer read the full documentation at [https://www.kimai.org/documentation/migration-v2.html](https://www.kimai.org/documentation/migration-v2.html).
|
||||
|
||||
- Invoice renderer and templates for XML, JSON and TEXT were moved to the [Extended invoicing plugin](https://www.kimai.org/store/invoice-bundle.html) (install if you use one of those)
|
||||
- Moved `company.docx` to [external repo](https://github.com/kimai/invoice-templates/tree/main/docx-company) (needs to be re-uploaded if you want to keep on using it!)
|
||||
- Role names are forced to be uppercase
|
||||
- Removed unused `public/avatars/` directory
|
||||
- Time-tracking mode `duration_only` was removed, existing installations will be switched to `duration_fixed_begin`
|
||||
- Removed Twig filters. You might have to replace them in your custom export/invoice templates:
|
||||
- `date_full` => `date_time`
|
||||
- `duration_decimal` => `duration(true)`
|
||||
- `currency` => `currency_name`
|
||||
- `country` => `country_name`
|
||||
- `language` => `language_name`
|
||||
- Removed support for custom translation files (use [TranslationBundle](https://www.kimai.org/store/translation-bundle.html) instead or write your own plugin)
|
||||
- Removed all 3rd party mailer packages, you need to install them manually (ONLY if you used a short syntax to configure the `MAILER_URL` in `.env`):
|
||||
- `composer require symfony/amazon-mailer`
|
||||
- `composer require symfony/google-mailer`
|
||||
- `composer require symfony/mailchimp-mailer`
|
||||
- `composer require symfony/mailgun-mailer`
|
||||
- `composer require symfony/postmark-mailer`
|
||||
- `composer require symfony/sendgrid-mailer`
|
||||
The most recent version is Kimai 3, please read the changelog at [UPGRADING-3.md](UPGRADING-3.md).
|
||||
|
||||
5
assets/highlight.js
Normal file
5
assets/highlight.js
Normal file
@@ -0,0 +1,5 @@
|
||||
require('highlight.js/styles/github-dark.css');
|
||||
|
||||
const hljs = require('highlight.js/lib/common');
|
||||
|
||||
global.hljs = hljs;
|
||||
@@ -1,2 +1,4 @@
|
||||
|
||||
/**
|
||||
* @deprecated use invoice-pdf instead
|
||||
*/
|
||||
require('./sass/_invoice.scss');
|
||||
|
||||
@@ -43,6 +43,7 @@ import KimaiHotkeys from "./plugins/KimaiHotkeys";
|
||||
import KimaiRemoteModal from "./plugins/KimaiRemoteModal";
|
||||
import KimaiUser from "./plugins/KimaiUser";
|
||||
import KimaiAutocompleteTags from "./forms/KimaiAutocompleteTags";
|
||||
import KimaiMonthPicker from "./forms/KimaiMonthPicker";
|
||||
|
||||
export default class KimaiLoader {
|
||||
|
||||
@@ -76,6 +77,7 @@ export default class KimaiLoader {
|
||||
kimai.registerPlugin(new KimaiTeamForm());
|
||||
kimai.registerPlugin(new KimaiCopyDataForm());
|
||||
kimai.registerPlugin(new KimaiDateNowForm());
|
||||
kimai.registerPlugin(new KimaiMonthPicker());
|
||||
kimai.registerPlugin(new KimaiForm());
|
||||
kimai.registerPlugin(new KimaiHotkeys());
|
||||
|
||||
@@ -85,7 +87,7 @@ export default class KimaiLoader {
|
||||
kimai.registerPlugin(new KimaiDatatable('section.content', 'table.dataTable'));
|
||||
kimai.registerPlugin(new KimaiToolbar('form.searchform', 'toolbar-action'));
|
||||
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
|
||||
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
|
||||
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form', ['td.multiCheckbox', 'td.actions']));
|
||||
kimai.registerPlugin(new KimaiRemoteModal());
|
||||
kimai.registerPlugin(new KimaiActiveRecords());
|
||||
kimai.registerPlugin(new KimaiAPILink('api-link'));
|
||||
|
||||
@@ -154,7 +154,7 @@ export default class KimaiPlugin {
|
||||
const width = Math.max(
|
||||
document.documentElement.clientWidth,
|
||||
window.innerWidth || 0
|
||||
)
|
||||
);
|
||||
|
||||
return width < 576;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default class KimaiCopyDataForm extends KimaiFormPlugin {
|
||||
/**
|
||||
* @param {HTMLFormElement} form
|
||||
*/
|
||||
activateForm(form) // eslint-disable-line no-unused-vars
|
||||
activateForm(form)
|
||||
{
|
||||
if (this._eventHandler === undefined) {
|
||||
this._eventHandler = (event) => {
|
||||
@@ -48,10 +48,10 @@ export default class KimaiCopyDataForm extends KimaiFormPlugin {
|
||||
if (element.dataset.event !== undefined) {
|
||||
for (const event of element.dataset.event.split(' ')) {
|
||||
target.dispatchEvent(new Event(event));
|
||||
}
|
||||
} else if (element.dataset.eventBubbles !== undefined) {
|
||||
for (const event of element.dataset.eventBubbles.split(' ')) {
|
||||
target.dispatchEvent(new Event(event, {bubbles: true}));
|
||||
const form = target.closest('form');
|
||||
if (form !== null) {
|
||||
form.dispatchEvent(new Event(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
@@ -63,7 +63,7 @@ export default class KimaiCopyDataForm extends KimaiFormPlugin {
|
||||
/**
|
||||
* @param {HTMLFormElement} form
|
||||
*/
|
||||
destroyForm(form) // eslint-disable-line no-unused-vars
|
||||
destroyForm(form)
|
||||
{
|
||||
form.removeEventListener('click', this._eventHandler);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,10 @@ export default class KimaiDateNowForm extends KimaiFormPlugin {
|
||||
const formElement = document.getElementById(linkTarget.dataset.target);
|
||||
if (!formElement.disabled) {
|
||||
formElement.value = this.getDateUtils().format(linkTarget.dataset.format, null);
|
||||
// this should usually work
|
||||
formElement.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
// this is required for Litepicker to pick the new date (with autoRefresh option)
|
||||
formElement.dispatchEvent(new Event('keyup', {bubbles: true}));
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
@@ -62,13 +62,10 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
|
||||
plugins.push('remove_button');
|
||||
}
|
||||
|
||||
/*
|
||||
const isOrdering = false;
|
||||
if (isOrdering) {
|
||||
plugins.push('caret_position');
|
||||
if (node.dataset['order'] !== undefined && node.dataset['order'] === '1') {
|
||||
//plugins.push('caret_position');
|
||||
plugins.push('drag_drop');
|
||||
}
|
||||
*/
|
||||
|
||||
let options = {
|
||||
// see https://github.com/orchidjs/tom-select/issues/543#issuecomment-1664342257
|
||||
@@ -83,8 +80,11 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
|
||||
// if there are more than X entries, the other ones are hidden and can only be found
|
||||
// by typing some characters to trigger the internal option search
|
||||
// see App\Form\Type\TagsType::MAX_AMOUNT_SELECT
|
||||
// TODO make this value configurable with a data attribute
|
||||
maxOptions: 500,
|
||||
sortField:[{field: '$order'}, {field: '$score'}],
|
||||
// required so it works in table.responsive, but requires z-index 1056, because bootstrap modal would otherwise hide it
|
||||
dropdownParent: 'body',
|
||||
};
|
||||
|
||||
let render = {
|
||||
@@ -236,8 +236,13 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
|
||||
options.push(optGroup);
|
||||
}
|
||||
|
||||
// log the one with a group name first (e.g. non-global activities)
|
||||
options.forEach(child => node.appendChild(child));
|
||||
emptyOpts.forEach(child => node.appendChild(child));
|
||||
|
||||
// append the ones with no parent at the end (e.g. global activities)
|
||||
const optGroupEmpty = this._createOptgroup('');
|
||||
emptyOpts.forEach(child => optGroupEmpty.appendChild(child));
|
||||
node.appendChild(optGroupEmpty);
|
||||
|
||||
// if available, re-select the previous selected option (mostly usable for global activities)
|
||||
node.value = selectedValue;
|
||||
@@ -435,7 +440,7 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
|
||||
targetSelect.dataset['reloading'] = '0';
|
||||
targetSelect.disabled = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('change', this._eventHandlerApiSelects);
|
||||
}
|
||||
@@ -471,14 +476,14 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
|
||||
newValue = [...targetField.selectedOptions].map(o => o.value);
|
||||
} else if (newValue !== '') {
|
||||
if (targetField.type === 'date') {
|
||||
const timeId = targetField.id.replace('_date', '_time')
|
||||
const timeId = targetField.id.replace('_date', '_time');
|
||||
const timeElement = document.getElementById(timeId);
|
||||
const time = timeElement === null ? '12:00:00' : timeElement.value;
|
||||
// using 12:00 as fallback, because timezone handling might change the date if we use 00:00
|
||||
const newDate = this.getDateUtils().fromHtml5Input(newValue, time);
|
||||
newValue = this.getDateUtils().formatForAPI(newDate, false);
|
||||
} else if (targetField.type === 'text' && targetField.name.includes('date')) {
|
||||
const timeId = targetField.id.replace('_date', '_time')
|
||||
const timeId = targetField.id.replace('_date', '_time');
|
||||
const timeElement = document.getElementById(timeId);
|
||||
// using 12:00 as fallback, because timezone handling might change the date if we use 00:00
|
||||
let time = '12:00:00';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiFormPlugin: base class for all none ID plugin that handle forms
|
||||
* [KIMAI] KimaiFormTomselectPlugin: base form plugin for everyone using tomselect
|
||||
*/
|
||||
|
||||
import KimaiFormPlugin from './KimaiFormPlugin';
|
||||
@@ -28,12 +28,12 @@ export default class KimaiFormTomselectPlugin extends KimaiFormPlugin {
|
||||
return null;
|
||||
}
|
||||
const tpl = this.translate('select.search.create');
|
||||
const tplReplaced = tpl.replace('%input%', '<strong>' + name + '</strong>')
|
||||
const tplReplaced = tpl.replace('%input%', '<strong>' + name + '</strong>');
|
||||
return '<div class="create">' + tplReplaced + '</div>';
|
||||
},
|
||||
no_results: (data, escape) => {
|
||||
const tpl = this.translate('select.search.notfound');
|
||||
const tplReplaced = tpl.replace('%input%', '<strong>' + escape(data.input) + '</strong>')
|
||||
const tplReplaced = tpl.replace('%input%', '<strong>' + escape(data.input) + '</strong>');
|
||||
return '<div class="no-results">' + tplReplaced + '</div>';
|
||||
},
|
||||
};
|
||||
@@ -44,7 +44,7 @@ export default class KimaiFormTomselectPlugin extends KimaiFormPlugin {
|
||||
let item = '<div class="list-group-item border-0 p-1 ps-2 text-nowrap">';
|
||||
// if no color is set, do NOT add an empty placeholder
|
||||
if (data.color !== undefined) {
|
||||
item += '<span style="background-color:' + data.color + '" class="color-choice-item"> </span>';
|
||||
item += '<span style="background-color:' + data.color + '" class="color-choice-item me-2"> </span>';
|
||||
}
|
||||
item += escape(data.text) + '</div>';
|
||||
return item;
|
||||
@@ -53,7 +53,7 @@ export default class KimaiFormTomselectPlugin extends KimaiFormPlugin {
|
||||
let item = '<div class="text-nowrap">';
|
||||
// if no color is set, do NOT add an empty placeholder
|
||||
if (data.color !== undefined) {
|
||||
item += '<span style="background-color:' + data.color + '" class="color-choice-item"> </span>';
|
||||
item += '<span style="background-color:' + data.color + '" class="color-choice-item me-2"> </span>';
|
||||
}
|
||||
item += escape(data.text) + '</div>';
|
||||
return item;
|
||||
|
||||
44
assets/js/forms/KimaiMonthPicker.js
Normal file
44
assets/js/forms/KimaiMonthPicker.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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] KimaiDatePicker: single date selects (currently unused)
|
||||
*/
|
||||
|
||||
import KimaiFormPlugin from "./KimaiFormPlugin";
|
||||
|
||||
export default class KimaiMonthPicker extends KimaiFormPlugin {
|
||||
|
||||
/**
|
||||
* @param {HTMLFormElement} form
|
||||
* @return boolean
|
||||
*/
|
||||
supportsForm(form) // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLFormElement} form
|
||||
*/
|
||||
activateForm(form)
|
||||
{
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type','month');
|
||||
|
||||
const notADateValue = 'not-a-month';
|
||||
input.setAttribute('value', notADateValue);
|
||||
|
||||
if (input.value === notADateValue) {
|
||||
const polyfills = form.querySelectorAll('a.input-month-polyfill');
|
||||
polyfills.forEach(i => i.classList.toggle('d-none'));
|
||||
const inputs = form.querySelectorAll('input[type="month"]');
|
||||
inputs.forEach(i => i.classList.toggle('d-none'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -40,17 +40,27 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
|
||||
if (this._beginTime !== undefined) {
|
||||
this._beginTime.removeEventListener('change', this._beginListener);
|
||||
delete this._beginListener;
|
||||
this._beginTime.removeEventListener('blur', this._beginBlurListener);
|
||||
delete this._beginBlurListener;
|
||||
delete this._beginTime;
|
||||
}
|
||||
|
||||
if (this._endTime !== undefined) {
|
||||
this._endTime.removeEventListener('change', this._endListener);
|
||||
delete this._endListener;
|
||||
this._endTime.removeEventListener('blur', this._endBlurListener);
|
||||
delete this._endBlurListener;
|
||||
delete this._endTime;
|
||||
}
|
||||
|
||||
if (this._duration !== undefined) {
|
||||
this._duration.removeEventListener('change', this._durationListener);
|
||||
delete this._durationListener;
|
||||
this._duration.removeEventListener('keydown', this._durationKeyListener);
|
||||
delete this._durationKeyListener;
|
||||
this._duration.removeEventListener('blur', this._durationBlurListener);
|
||||
delete this._durationBlurListener;
|
||||
delete this._duration;
|
||||
}
|
||||
|
||||
@@ -108,13 +118,21 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
}
|
||||
|
||||
this._beginListener = () => this._changedBegin();
|
||||
this._beginBlurListener = () => this._parseBeginTime();
|
||||
this._endListener = () => this._changedEnd();
|
||||
this._endBlurListener = () => this._parseEndTime();
|
||||
this._durationListener = () => this._changedDuration();
|
||||
this._durationKeyListener = (event) => this._changeDurationOnKeypress(event);
|
||||
this._durationBlurListener = () => this._parseDuration();
|
||||
|
||||
this._beginDate.addEventListener('change', this._beginListener);
|
||||
this._beginTime.addEventListener('change', this._beginListener);
|
||||
this._beginTime.addEventListener('blur', this._beginBlurListener);
|
||||
this._endTime.addEventListener('change', this._endListener);
|
||||
this._endTime.addEventListener('blur', this._endBlurListener);
|
||||
this._duration.addEventListener('change', this._durationListener);
|
||||
this._duration.addEventListener('keydown', this._durationKeyListener);
|
||||
this._duration.addEventListener('blur', this._durationBlurListener);
|
||||
|
||||
if (this._duration !== null && this._durationToggle !== null) {
|
||||
this._durationToggleListener = () => {
|
||||
@@ -124,6 +142,159 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
_parseBeginTime()
|
||||
{
|
||||
if (this._beginTime.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
let newBeginTime = this._formatTimeForParsing(this._beginTime.value, this._beginTime.dataset['format']);
|
||||
if (newBeginTime !== this._beginTime.value) {
|
||||
this._beginTime.value = newBeginTime;
|
||||
this._changedBegin();
|
||||
}
|
||||
}
|
||||
|
||||
_parseEndTime()
|
||||
{
|
||||
if (this._endTime.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
let newEndTime = this._formatTimeForParsing(this._endTime.value, this._endTime.dataset['format']);
|
||||
if (newEndTime !== this._endTime.value) {
|
||||
this._endTime.value = newEndTime;
|
||||
this._changedEnd();
|
||||
}
|
||||
}
|
||||
|
||||
_parseDuration()
|
||||
{
|
||||
if (this._duration.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setDurationAsString(this._getParsedDuration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a time, written by a human, probably in an invalid format.
|
||||
* This method supports 12-hour or 24-hour format, the format string contains an uppercase "A" in case of the 12-hour format.
|
||||
* If it is 12-hour format, then always en-US locallized with AM/PM.
|
||||
*
|
||||
* Ruleset:
|
||||
* - Some locales use a dot instead of a colon, always replace the dot in HH.mm with a colon as in HH:mm
|
||||
* - If there is an "am" or "pm", always uppercase them
|
||||
* - Split the string into time and prefix: if AM/PM is included remove it and remember for later
|
||||
* - If the time is a 1 or 2 character long number: use as hours
|
||||
* - If the time now is 3 character long: use the 1 char as hour and the 2 and 3 char as minute
|
||||
* - If the time now is 4 character long: use the 1 and 2 char as hour and the 3 and 4 char as minutes
|
||||
* - If the format is 12-hour: try to identify the correct time and suffix
|
||||
* - If the format is 12-hour and misses the AM/PM: try to detect whether it
|
||||
* - If the time contains AM or PM, make sure that it is always prefixed by a space character
|
||||
*
|
||||
* @param {string} time
|
||||
* @param {string} format
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
_formatTimeForParsing(time, format)
|
||||
{
|
||||
let formatted = time.trim();
|
||||
|
||||
// replace invalid separators with colon
|
||||
formatted = formatted.replace(/\.|;|,/g, ':');
|
||||
// uppercase 12-hour format
|
||||
formatted = formatted.replace(/am/i, 'AM');
|
||||
formatted = formatted.replace(/pm/i, 'PM');
|
||||
|
||||
// Split time and AM/PM suffix if present
|
||||
let suffix = '';
|
||||
let hour = 0;
|
||||
let minute = 0;
|
||||
|
||||
let timePart = formatted;
|
||||
const ampmMatch = formatted.match(/\s*(AM|PM)$/i);
|
||||
if (ampmMatch) {
|
||||
suffix = ampmMatch[1].toUpperCase();
|
||||
timePart = formatted.replace(/\s*(AM|PM)$/i, '').trim();
|
||||
}
|
||||
|
||||
if (timePart.indexOf(':') !== -1) {
|
||||
const match = timePart.match(/(?:(\d+):)?(\d+)/);
|
||||
hour = parseInt(match?.[1] || 0, 10);
|
||||
minute = parseInt(match?.[2] || 0, 10);
|
||||
} else {
|
||||
timePart = timePart.replace(/:/, '');
|
||||
|
||||
if (/^\d{1,2}$/.test(timePart)) {
|
||||
hour = timePart;
|
||||
}
|
||||
|
||||
if (/^\d{3}$/.test(timePart)) {
|
||||
hour = timePart.slice(0, 1);
|
||||
minute = timePart.slice(1);
|
||||
}
|
||||
|
||||
if (/^\d{4}$/.test(timePart)) {
|
||||
hour = timePart.slice(0, 2);
|
||||
minute = timePart.slice(2);
|
||||
}
|
||||
}
|
||||
|
||||
hour = parseInt(hour);
|
||||
minute = parseInt(minute);
|
||||
|
||||
// just in case a person entered a wrong time like 35 hours
|
||||
hour = hour % 24;
|
||||
minute = minute % 60;
|
||||
|
||||
// format is 12-hour
|
||||
if (format.toUpperCase().indexOf('A') !== -1) {
|
||||
// time entered in 24-hour: convert to 12-hour format
|
||||
if (hour > 12 && hour < 24) {
|
||||
hour = hour - 12;
|
||||
suffix = 'PM';
|
||||
}
|
||||
|
||||
// if the person forgot to add a suffix, calculate it and convert time
|
||||
if (suffix === '') {
|
||||
if (hour === 0) {
|
||||
hour = 12;
|
||||
suffix = 'AM';
|
||||
} else if (hour === 12) {
|
||||
suffix = 'PM';
|
||||
} else {
|
||||
suffix = 'AM';
|
||||
}
|
||||
}
|
||||
|
||||
if (suffix === 'PM' && hour === 0) {
|
||||
hour = 12;
|
||||
}
|
||||
|
||||
} else {
|
||||
// this is the 34-hour format branch
|
||||
|
||||
// check if the person entered time in 12-hour format and convert it
|
||||
if (suffix === 'AM' && hour === 12) {
|
||||
hour = 0;
|
||||
} else if (suffix === 'PM' && hour !== 12) {
|
||||
hour = (hour + 12) % 24;
|
||||
}
|
||||
|
||||
// make sure we have no suffix
|
||||
suffix = '';
|
||||
}
|
||||
|
||||
formatted = hour + ':' + minute.toString().padStart(2, '0');
|
||||
if (suffix !== '') {
|
||||
formatted = formatted + ' ' + suffix.trim();
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
_isDurationConnected()
|
||||
{
|
||||
if (this._duration === null && this._durationToggle === null) {
|
||||
@@ -287,14 +458,14 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
|
||||
/**
|
||||
* Ruleset:
|
||||
* - invalid duration => skip
|
||||
* - invalid or empty duration => skip
|
||||
* - if begin and end are empty: set begin to now and end to duration
|
||||
* - if begin is empty and end is not empty: set begin to end minus duration
|
||||
* - if begin is not empty and end is empty and duration is > 0 (running records = 0): set end to begin plus duration
|
||||
*/
|
||||
_changedDuration()
|
||||
{
|
||||
if (!this._isDurationConnected()) {
|
||||
if (!this._isDurationConnected() || this._duration.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -315,11 +486,11 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
if (begin === null && end === null) {
|
||||
const newBegin = DateTime.now();
|
||||
this._applyDateToField(newBegin, this._beginDate, this._beginTime);
|
||||
this._applyDateToField(newBegin.plus({seconds: seconds}), null, this._endTime);
|
||||
this._addSecondsToEndDate(newBegin, seconds);
|
||||
} else if (begin === null && end !== null) {
|
||||
this._applyDateToField(end.minus({seconds: seconds}), this._beginDate, this._beginTime);
|
||||
} else if (begin !== null && seconds >= 0) {
|
||||
this._applyDateToField(begin.plus({seconds: seconds}), null, this._endTime);
|
||||
this._addSecondsToEndDate(begin, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +538,23 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
*/
|
||||
_getParsedDuration()
|
||||
{
|
||||
return this.getDateUtils().parseDuration(this._duration.value.toUpperCase());
|
||||
return this.getDateUtils().parseDuration(this._duration.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DateTime} dateTime
|
||||
* @param {int} seconds
|
||||
* @private
|
||||
*/
|
||||
_addSecondsToEndDate(dateTime, seconds)
|
||||
{
|
||||
// if the duration is longer than one day, the end field should be empty
|
||||
// so kimai can calculate it after submitting the data from start + duration
|
||||
if (seconds < 86400) {
|
||||
this._applyDateToField(dateTime.plus({seconds: seconds}), null, this._endTime);
|
||||
} else {
|
||||
this._endTime.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,4 +577,122 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
timeField.value = this.getDateUtils().format(timeField.dataset['format'], dateTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
* @private
|
||||
*/
|
||||
_changeDurationOnKeypress(event)
|
||||
{
|
||||
switch (event.key) {
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
case 'PageUp':
|
||||
case 'PageDown':
|
||||
case 'Home':
|
||||
case 'End':
|
||||
this._setDurationAsString(this._getParsedDuration());
|
||||
break;
|
||||
default:
|
||||
return; // Ignore other keys
|
||||
}
|
||||
|
||||
this._changeTimeOnKeypress(event, this._duration, 99999, this._durationListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method helps the user to change a duration field with simple keyboard interaction:
|
||||
* - Read the current duration from the given timeField input in format HH:MM (no seconds)
|
||||
* - Change the duration based on the rules below
|
||||
* - Write the new duration back to the field
|
||||
* - If the field is empty or invalid it uses 00:00 as start-time
|
||||
* - Duration cannot exceed maxtime (which is given in minutes)
|
||||
* - Duration cannot drop below 00:00
|
||||
* - Read the position of the cursor and decide whether to increase minutes or hours: if the cursor is in the hour section (before the colon) change hours, if the cursor is in the minute section (after the colon) change minutes
|
||||
* - It reads the pressed key from the given KeyboardEvent and changes the duration accordingly to the rules below
|
||||
*
|
||||
* Rules to apply when a key is pressed:
|
||||
* - ArrowUp key to increase the duration (either 5 minutes or 1 hour, depending on the cursor position)
|
||||
* - ArrowDown key to decrease the duration (either 5 minutes or 1 hour, depending on the cursor position)
|
||||
* - PageUp key to increase the duration by 1 hour
|
||||
* - PageDown key to decrease the duration by 1 hour
|
||||
* - Home key to set the duration to 08:00
|
||||
* - End key to set the duration to 00:00
|
||||
* - all other keys are ignored
|
||||
*
|
||||
* @param {KeyboardEvent} event
|
||||
* @param {HTMLElement} timeField
|
||||
* @param {int} maxTime
|
||||
* @param {function} changeCallback
|
||||
* @private
|
||||
*/
|
||||
_changeTimeOnKeypress(event, timeField, maxTime, changeCallback)
|
||||
{
|
||||
// Parse current value or default to 00:00
|
||||
let value = timeField.value || '00:00';
|
||||
let [hours, minutes] = value.split(':').map(Number);
|
||||
if (isNaN(hours)) { hours = 0; }
|
||||
if (isNaN(minutes)) { minutes = 0; }
|
||||
|
||||
// Cursor position: before or after colon
|
||||
const cursorPos = timeField.selectionStart || 0;
|
||||
const colonPos = value.indexOf(':');
|
||||
const inHour = cursorPos <= colonPos;
|
||||
|
||||
// Helper to clamp values
|
||||
const clamp = (h, m) => {
|
||||
let total = h * 60 + m;
|
||||
if (total < 0) { total = 0; }
|
||||
if (total > maxTime) { total = maxTime; }
|
||||
h = Math.floor(total / 60);
|
||||
m = total % 60;
|
||||
return [h, m];
|
||||
};
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowUp':
|
||||
if (inHour) {
|
||||
[hours, minutes] = clamp(hours + 1, minutes);
|
||||
} else {
|
||||
[hours, minutes] = clamp(hours, minutes + 5);
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (inHour) {
|
||||
[hours, minutes] = clamp(hours - 1, minutes);
|
||||
} else {
|
||||
[hours, minutes] = clamp(hours, minutes - 5);
|
||||
}
|
||||
break;
|
||||
case 'PageUp':
|
||||
[hours, minutes] = clamp(hours + 1, minutes);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'PageDown':
|
||||
[hours, minutes] = clamp(hours - 1, minutes);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'Home':
|
||||
// TODO this should use the configured working time for today
|
||||
hours = 8;
|
||||
minutes = 0;
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'End':
|
||||
hours = 0;
|
||||
minutes = 0;
|
||||
event.preventDefault();
|
||||
break;
|
||||
default:
|
||||
return; // Ignore other keys
|
||||
}
|
||||
|
||||
// Format and set value
|
||||
timeField.value = `${hours}:${minutes.toString().padStart(2, '0')}`;
|
||||
// trigger update of linked fields
|
||||
changeCallback(timeField);
|
||||
// Move cursor to original position if possible
|
||||
setTimeout(() => {
|
||||
timeField.setSelectionRange(cursorPos, cursorPos);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ export default class KimaiAPILink extends KimaiPlugin {
|
||||
const ALERT = this.getContainer().getPlugin('alert');
|
||||
const successHandle = () => {
|
||||
EVENTS.trigger(eventName);
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
if (attributes['msgSuccess'] !== undefined) {
|
||||
ALERT.success(attributes['msgSuccess']);
|
||||
}
|
||||
@@ -82,6 +83,7 @@ export default class KimaiAPILink extends KimaiPlugin {
|
||||
if (attributes['msgError'] !== undefined) {
|
||||
message = attributes['msgError'];
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
API.handleError(message, error);
|
||||
};
|
||||
|
||||
@@ -90,6 +92,8 @@ export default class KimaiAPILink extends KimaiPlugin {
|
||||
data = attributes['payload'];
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
|
||||
|
||||
if (method === 'PATCH') {
|
||||
API.patch(url, data, successHandle, errorHandle);
|
||||
} else if (method === 'POST') {
|
||||
|
||||
@@ -17,9 +17,10 @@ import { Modal } from 'bootstrap';
|
||||
|
||||
export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
|
||||
constructor(selector) {
|
||||
constructor(selector, stopSelector) {
|
||||
super();
|
||||
this._selector = selector;
|
||||
this._stopSelector = stopSelector;
|
||||
}
|
||||
|
||||
getId()
|
||||
@@ -63,12 +64,12 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
|
||||
this.addClickHandler(this._selector, (href) => {
|
||||
this.openUrlInModal(href);
|
||||
});
|
||||
}, this._stopSelector);
|
||||
}
|
||||
|
||||
_getModal()
|
||||
{
|
||||
return Modal.getOrCreateInstance(this._getModalElement())
|
||||
return Modal.getOrCreateInstance(this._getModalElement());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,8 +217,6 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
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();
|
||||
@@ -258,15 +257,8 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -276,6 +268,8 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
message = 'action.update.error';
|
||||
}
|
||||
|
||||
/** @type {KimaiAlert} alert */
|
||||
const alert = this.getContainer().getPlugin('alert');
|
||||
alert.error(message, error.message);
|
||||
|
||||
// this is useful for changing form fields and retrying to save (and in development to test form changes)
|
||||
|
||||
@@ -208,7 +208,7 @@ export default class KimaiAlert extends KimaiPlugin {
|
||||
const toast = new Toast(element);
|
||||
element.addEventListener('hidden.bs.toast', function () {
|
||||
container.removeChild(element);
|
||||
})
|
||||
});
|
||||
toast.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class KimaiAlternativeLinks extends KimaiReducedClickHandler {
|
||||
init() {
|
||||
this.addClickHandler(this._selector, function(href) {
|
||||
window.location = href;
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -273,7 +273,10 @@ export default class KimaiDateUtils extends KimaiPlugin {
|
||||
let luxonDuration = null;
|
||||
|
||||
if (duration.indexOf(':') !== -1) {
|
||||
const [, hours, minutes, seconds] = duration.match(/(\d+):(\d+)(?::(\d+))*/);
|
||||
const match = duration.match(/(?:(\d+):)?(\d+)(?::(\d+))?/);
|
||||
const hours = parseInt(match?.[1] || 0, 10);
|
||||
const minutes = parseInt(match?.[2] || 0, 10);
|
||||
const seconds = parseInt(match?.[3] || 0, 10);
|
||||
luxonDuration = Duration.fromObject({hours: hours, minutes: minutes, seconds: seconds});
|
||||
} else if (duration.indexOf('.') !== -1 || duration.indexOf(',') !== -1) {
|
||||
duration = duration.replace(/,/, '.');
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class KimaiHotkeys extends KimaiPlugin {
|
||||
elements[0].click();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// adopted from Bootstrap 5.1.1, MIT
|
||||
|
||||
@@ -13,9 +13,14 @@ import KimaiPlugin from '../KimaiPlugin';
|
||||
|
||||
export default class KimaiMultiUpdateTable extends KimaiPlugin {
|
||||
|
||||
getId()
|
||||
{
|
||||
return 'datatable-batch-action';
|
||||
}
|
||||
|
||||
init()
|
||||
{
|
||||
if (document.getElementById('multi_update_all') === null) {
|
||||
if (document.getElementsByClassName('multi_update_all').length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,64 +28,101 @@ export default class KimaiMultiUpdateTable extends KimaiPlugin {
|
||||
// via KimaiDatable and everything inside will be removed, including event listeners
|
||||
const element = document.querySelector('div.page-body');
|
||||
element.addEventListener('change', (event) => {
|
||||
if (event.target.matches('#multi_update_all')) {
|
||||
// the "check all" checkbox in the upper start corner of the table
|
||||
const checked = event.target.checked;
|
||||
for (const element of document.querySelectorAll('.multi_update_single')) {
|
||||
element.checked = checked;
|
||||
}
|
||||
this._toggleForm();
|
||||
if (event.target.matches('.multi_update_all')) {
|
||||
this.toggle(event.target.checked, event.target.closest('table'));
|
||||
event.stopPropagation();
|
||||
} else if (event.target.matches('.multi_update_single')) {
|
||||
// single checkboxes in front of each row
|
||||
this._toggleForm();
|
||||
this._toggleDatatable(event.target.closest('table'));
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
element.addEventListener('click', (event) => {
|
||||
if (event.target.matches('.multi_update_table_action')) {
|
||||
const selectedItem = event.target;
|
||||
const ids = this._getSelectedIds();
|
||||
const form = document.getElementById('multi_update_form');
|
||||
const question = form.dataset['question'].replace(/%action%/, selectedItem.textContent).replace(/%count%/, ids.length.toString());
|
||||
const selectedButton = event.target;
|
||||
const form = selectedButton.form;
|
||||
const ids = form.querySelector('.multi_update_ids').value.split(',');
|
||||
const question = form.dataset['question'].replace(/%action%/, selectedButton.textContent).replace(/%count%/, ids.length.toString());
|
||||
|
||||
/** @type {KimaiAlert} ALERT */
|
||||
const ALERT = this.getPlugin('alert');
|
||||
ALERT.question(question, function(value) {
|
||||
if (value) {
|
||||
const form = document.getElementById('multi_update_form');
|
||||
form.action = selectedItem.dataset['href'];
|
||||
form.action = selectedButton.dataset['href'];
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getSelectedIds()
|
||||
|
||||
/**
|
||||
* @param {boolean} checked
|
||||
* @param {HTMLTableElement} table
|
||||
*/
|
||||
toggle(checked, table)
|
||||
{
|
||||
for (const element of table.querySelectorAll('.multi_update_single')) {
|
||||
element.checked = checked;
|
||||
}
|
||||
this._toggleDatatable(table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} checked
|
||||
*/
|
||||
toggleAll(checked)
|
||||
{
|
||||
for (const element of document.querySelectorAll('.multi_update_all')) {
|
||||
this._toggleAll(checked, element);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} checked
|
||||
* @param {string} name
|
||||
*/
|
||||
toggleByName(checked, name)
|
||||
{
|
||||
for (const element of document.querySelectorAll('#multi_update_all_' + name)) {
|
||||
this._toggleAll(checked, element);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} checked
|
||||
* @param {Element} name
|
||||
*/
|
||||
_toggleAll(checked, element)
|
||||
{
|
||||
element.checked = checked;
|
||||
this.toggle(checked, element.closest('table'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLTableElement} table
|
||||
* @private
|
||||
*/
|
||||
_toggleDatatable(table)
|
||||
{
|
||||
const card = table.closest('div.card.data_table');
|
||||
|
||||
let ids = [];
|
||||
for (const box of document.querySelectorAll('input.multi_update_single:checked')) {
|
||||
for (const box of table.querySelectorAll('input.multi_update_single:checked')) {
|
||||
ids.push(box.value);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
_toggleForm()
|
||||
{
|
||||
const ids = this._getSelectedIds();
|
||||
document.getElementById('multi_update_table_entities').value = ids.join(',');
|
||||
card.querySelector('.multi_update_ids').value = ids.join(',');
|
||||
|
||||
if (ids.length > 0) {
|
||||
for (const element of document.getElementsByClassName('multi_update_form_hide')) {
|
||||
for (const element of card.querySelectorAll('.multi_update_form_hide')) {
|
||||
element.style.setProperty('display', 'none', 'important');
|
||||
}
|
||||
document.getElementById('multi_update_form').style.display = null;//'block';
|
||||
card.querySelector('form.multi_update_form').style.display = null;//'block';
|
||||
} else {
|
||||
document.getElementById('multi_update_form').style.setProperty('display', 'none', 'important');
|
||||
for (const element of document.getElementsByClassName('multi_update_form_hide')) {
|
||||
card.querySelector('form.multi_update_form').style.setProperty('display', 'none', 'important');
|
||||
for (const element of card.querySelectorAll('.multi_update_form_hide')) {
|
||||
element.style.display = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export default class KimaiNotification extends KimaiPlugin {
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (e) { // eslint-disable-line no-unused-vars
|
||||
Notification.requestPermission((permission) => {
|
||||
if (permission === "granted") {
|
||||
callback(true);
|
||||
|
||||
@@ -15,10 +15,11 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
|
||||
|
||||
/**
|
||||
* No _underscore naming for now, as it would be mangled otherwise
|
||||
* @param selector
|
||||
* @param callback
|
||||
* @param {string} selector
|
||||
* @param {callback} callback
|
||||
* @param {array<string>} stopSelector
|
||||
*/
|
||||
addClickHandler(selector, callback) {
|
||||
addClickHandler(selector, callback, stopSelector) {
|
||||
document.body.addEventListener('click', (event) => {
|
||||
// event.currentTarget is ALWAYS the body
|
||||
|
||||
@@ -38,6 +39,12 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let x of stopSelector) {
|
||||
if (target.matches(x)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
target = target.parentNode;
|
||||
}
|
||||
|
||||
@@ -54,6 +61,12 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let x of stopSelector) {
|
||||
if (target.matches(x)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
@@ -6,12 +6,17 @@
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiRecentActivities: responsible to reload the users recent activities
|
||||
* [KIMAI] KimaiRemoteModal: load remote content (without forms) into a modal
|
||||
*/
|
||||
|
||||
import KimaiPlugin from '../KimaiPlugin';
|
||||
import { Modal } from 'bootstrap';
|
||||
|
||||
/**
|
||||
* Use like this:
|
||||
* <a href="{{ path('your-route') }}" class="remote-modal-load" data-modal-id="remote_modal" data-modal-class="p-0" data-modal-title="Some title" title="Some title">Modal</a>
|
||||
* <a href="{{ path('your-route') }}" class="remote-modal-load" data-modal-title="Some title" title="Some title">Modal</a>
|
||||
*/
|
||||
export default class KimaiRemoteModal extends KimaiPlugin {
|
||||
|
||||
constructor()
|
||||
@@ -34,43 +39,11 @@ export default class KimaiRemoteModal extends KimaiPlugin {
|
||||
this._showModal(event.currentTarget);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
for (let link of document.querySelectorAll(this._selector)) {
|
||||
link.addEventListener('click', this.handle);
|
||||
}
|
||||
|
||||
document.addEventListener('kimai.closeRemoteModal', () => { this._hide(); });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
* @private
|
||||
*/
|
||||
_initElement(element)
|
||||
{
|
||||
for (let link of element.querySelectorAll('a.remote-modal-reload')) {
|
||||
link.addEventListener('click', this.handle);
|
||||
}
|
||||
}
|
||||
|
||||
_hide()
|
||||
{
|
||||
this._getModal().hide();
|
||||
}
|
||||
|
||||
_getModalElement()
|
||||
{
|
||||
return document.getElementById('remote_modal');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Modal}
|
||||
* @private
|
||||
*/
|
||||
_getModal()
|
||||
{
|
||||
return Modal.getOrCreateInstance(this._getModalElement());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,21 +58,33 @@ export default class KimaiRemoteModal extends KimaiPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
let modalSelector = 'remote_modal';
|
||||
if (element.dataset['modalId'] !== undefined) {
|
||||
modalSelector = element.dataset['modalId'];
|
||||
}
|
||||
const modalElement = document.getElementById(modalSelector);
|
||||
if (modalElement === null) {
|
||||
console.log('Could not find modal with ID: ' + modalSelector);
|
||||
}
|
||||
|
||||
return response.text().then(html => {
|
||||
const newFormHtml = document.createElement('div');
|
||||
newFormHtml.classList.add('modal-body');
|
||||
newFormHtml.classList.add('p-0');
|
||||
newFormHtml.innerHTML = html;
|
||||
const modalBody = document.createElement('div');
|
||||
modalBody.classList.add('modal-body');
|
||||
if (element.dataset['modalClass'] !== undefined) {
|
||||
modalBody.classList.add(element.dataset['modalClass']);
|
||||
}
|
||||
modalBody.innerHTML = html;
|
||||
|
||||
this._initElement(newFormHtml);
|
||||
|
||||
const modal = this._getModalElement();
|
||||
modal.querySelector('.modal-body').replaceWith(newFormHtml);
|
||||
if (element.dataset['modalTitle'] !== undefined) {
|
||||
modal.querySelector('.modal-title').textContent = element.dataset['modalTitle'];
|
||||
for (let link of modalBody.querySelectorAll('a.remote-modal-reload')) {
|
||||
link.addEventListener('click', this.handle);
|
||||
}
|
||||
|
||||
this._getModal().show();
|
||||
modalElement.querySelector('.modal-body').replaceWith(modalBody);
|
||||
if (element.dataset['modalTitle'] !== undefined) {
|
||||
modalElement.querySelector('.modal-title').textContent = element.dataset['modalTitle'];
|
||||
}
|
||||
|
||||
Modal.getOrCreateInstance(modalElement).show();
|
||||
});
|
||||
})
|
||||
.catch((reason) => {
|
||||
|
||||
@@ -42,13 +42,13 @@ export default class KimaiThemeInitializer extends KimaiPlugin {
|
||||
}
|
||||
|
||||
// at which element we append the loading screen
|
||||
let container = 'body';
|
||||
let container = 'div.page-wrapper';
|
||||
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>';
|
||||
temp.innerHTML = '<div class="overlay"><div class="progress progress-sm"><div class="progress-bar progress-bar-indeterminate"></div></div></div>';
|
||||
this.overlay = temp.firstElementChild;
|
||||
document.querySelector(container).append(this.overlay);
|
||||
});
|
||||
|
||||
@@ -49,4 +49,11 @@ export default class KimaiUser extends KimaiPlugin {
|
||||
return this.user.superAdmin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {array}
|
||||
*/
|
||||
getRoles() {
|
||||
return this.user.roles;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import bootstrap5Plugin, { BootstrapTheme } from '@fullcalendar/bootstrap5';
|
||||
import googlePlugin from '@fullcalendar/google-calendar';
|
||||
import iCalendarPlugin from '@fullcalendar/icalendar'
|
||||
import iCalendarPlugin from '@fullcalendar/icalendar';
|
||||
import interactionPlugin, { Draggable } from '@fullcalendar/interaction';
|
||||
import arLocale from '@fullcalendar/core/locales/ar';
|
||||
import csLocale from '@fullcalendar/core/locales/cs';
|
||||
@@ -49,6 +49,7 @@ import enGbLocale from '@fullcalendar/core/locales/en-gb';
|
||||
import enUsLocale from '@fullcalendar/core/locales/en-gb';
|
||||
import KimaiColor from './KimaiColor';
|
||||
import KimaiContextMenu from "./KimaiContextMenu";
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export default class KimaiCalendar {
|
||||
|
||||
@@ -69,23 +70,6 @@ export default class KimaiCalendar {
|
||||
const DATES = this.kimai.getPlugin('date');
|
||||
/** @type {KimaiAjaxModalForm} MODAL */
|
||||
const MODAL = this.kimai.getPlugin('modal');
|
||||
/** @type {KimaiAlert} ALERT */
|
||||
const ALERT = this.kimai.getPlugin('alert');
|
||||
|
||||
let initialView = 'dayGridMonth';
|
||||
switch (options['initialView']) {
|
||||
case 'month':
|
||||
initialView = 'dayGridMonth';
|
||||
break;
|
||||
case 'agendaWeek':
|
||||
case 'week':
|
||||
initialView = 'timeGridWeek';
|
||||
break;
|
||||
case 'agendaDay':
|
||||
case 'day':
|
||||
initialView = 'timeGridDay';
|
||||
break;
|
||||
}
|
||||
|
||||
// Instead of using "buttonIcons" the theme needs to be adjusted directly
|
||||
// https://fullcalendar.io/docs/buttonIcons
|
||||
@@ -109,18 +93,18 @@ export default class KimaiCalendar {
|
||||
nextYear: this.options['icons']['nextYear'],
|
||||
};
|
||||
BootstrapTheme.prototype.rtlIconClasses = {
|
||||
prev: this.options['icons']['next'],
|
||||
next: this.options['icons']['previous'],
|
||||
prevYear: this.options['icons']['nextYear'],
|
||||
nextYear: this.options['icons']['previousYear'],
|
||||
prev: this.options['icons']['previous'],
|
||||
next: this.options['icons']['next'],
|
||||
prevYear: this.options['icons']['previousYear'],
|
||||
nextYear: this.options['icons']['nextYear'],
|
||||
};
|
||||
|
||||
let calendarOptions = {
|
||||
locales: [ enGbLocale, enUsLocale, arLocale, csLocale, daLocale, deLocale, deAtLocale, elLocale,
|
||||
esLocale, euLocale, faLocale, fiLocale, frLocale, heLocale, hrLocale, huLocale, itLocale, jaLocale, koLocale,
|
||||
nbLocale, nlLocale, plLocale, ptLocale, ptBrLocale, roLocale, ruLocale, skLocale, svLocale, trLocale, zhLocale, viLocale ],
|
||||
plugins: [ bootstrap5Plugin, dayGridPlugin, timeGridPlugin, googlePlugin, iCalendarPlugin, interactionPlugin ],
|
||||
initialView: initialView,
|
||||
initialView: this.toInternalViewName(this.options['initialView']),
|
||||
initialDate: this.options['initialDate'],
|
||||
// https://fullcalendar.io/docs/theming
|
||||
themeSystem: 'bootstrap5',
|
||||
// https://fullcalendar.io/docs/headerToolbar
|
||||
@@ -155,8 +139,9 @@ export default class KimaiCalendar {
|
||||
slotMinTime: this.options['timeframeBegin'] + ':00',
|
||||
slotMaxTime: this.options['timeframeEnd'] === '23:59' ? '24:00:00' : (this.options['timeframeEnd'] + ':59'),
|
||||
|
||||
// auto calculation seems to do the better job, therefor deactivated
|
||||
//slotLabelInterval: this.options['slotDuration'],
|
||||
// deactivate for auto calculation, which does a good job.
|
||||
// but 1h seems to be a "normal distance" for calendar apps (like Google and Apple)
|
||||
slotLabelInterval: '1:00',
|
||||
|
||||
// how long should entries look like when they don't have an end
|
||||
defaultTimedEventDuration: this.options['slotDuration'],
|
||||
@@ -172,14 +157,20 @@ export default class KimaiCalendar {
|
||||
// once we can configure working days
|
||||
// hiddenDays: [ 2, 4 ]
|
||||
|
||||
// when we support holidays and other full day events
|
||||
// allDaySlot: false,
|
||||
// dropAccept
|
||||
|
||||
dayMaxEventRows: true,
|
||||
eventMaxStack: this.options['dayLimit'],
|
||||
dayMaxEvents: this.options['dayLimit'],
|
||||
|
||||
// the callbacks "viewDidMount" and "viewWillUnmount" are only called when switching between month and others, not between week and day
|
||||
datesSet: (dateInfo) => {
|
||||
document.dispatchEvent(new CustomEvent('kimai.calendar.changeDate', {detail: {
|
||||
view: this.toExternalViewName(dateInfo.view.type),
|
||||
date: dateInfo.start.toISOString().split('T')[0],
|
||||
}}));
|
||||
},
|
||||
|
||||
views: {
|
||||
dayGrid: {
|
||||
dayMaxEventRows: this.options['dayLimit']
|
||||
@@ -265,8 +256,14 @@ export default class KimaiCalendar {
|
||||
contextMenu.createFromApi(jsEvent, result);
|
||||
}, (e) => { console.log('Failed to load actions for context menu', e); });
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
// called after all events of one source were set, so this can
|
||||
// and will be called multiple times before the calendar is initialized
|
||||
eventsSet: (events) => {
|
||||
this._renderDayAndWeekSum(this.getCalendar().getCurrentData().viewSpec.type, events);
|
||||
}
|
||||
};
|
||||
|
||||
// ============= DRAG & DROP =============
|
||||
@@ -283,6 +280,7 @@ export default class KimaiCalendar {
|
||||
droppable: true,
|
||||
// drop function handles external draggable events
|
||||
drop: (dropInfo) => {
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
|
||||
const entry = dropInfo.draggedEl;
|
||||
const source = entry.parentElement;
|
||||
let data = JSON.parse(entry.dataset.entry);
|
||||
@@ -325,7 +323,7 @@ export default class KimaiCalendar {
|
||||
(result) => {
|
||||
const newItem = this.convertSourceForCalendar(result);
|
||||
this.getCalendar().addEvent(newItem, true);
|
||||
ALERT.success('action.update.success');
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
}
|
||||
);
|
||||
} else {
|
||||
@@ -335,7 +333,7 @@ export default class KimaiCalendar {
|
||||
(result) => {
|
||||
const newItem = this.convertSourceForCalendar(result);
|
||||
this.getCalendar().addEvent(newItem, true);
|
||||
ALERT.success('action.update.success');
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -410,13 +408,13 @@ export default class KimaiCalendar {
|
||||
this.hidePopover(info.el);
|
||||
},
|
||||
eventDrop: (eventDropInfo) => {
|
||||
this.changeHandler(eventDropInfo)
|
||||
this.changeHandler(eventDropInfo);
|
||||
},
|
||||
eventResizeStart: (info) => {
|
||||
this.hidePopover(info.el);
|
||||
},
|
||||
eventResize: (eventResizeInfo) => {
|
||||
this.changeHandler(eventResizeInfo)
|
||||
this.changeHandler(eventResizeInfo);
|
||||
},
|
||||
}};
|
||||
}
|
||||
@@ -529,6 +527,44 @@ export default class KimaiCalendar {
|
||||
return (event.source.id.indexOf('kimai-') === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} viewName
|
||||
* @returns {string}
|
||||
*/
|
||||
toExternalViewName(viewName) {
|
||||
switch(viewName) {
|
||||
case 'timeGridDay':
|
||||
return 'day';
|
||||
case 'timeGridWeek':
|
||||
return 'week';
|
||||
case 'dayGridMonth':
|
||||
default:
|
||||
return 'month';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} viewName
|
||||
* @returns {string}
|
||||
*/
|
||||
toInternalViewName(viewName) {
|
||||
switch(viewName) {
|
||||
case 'day':
|
||||
case 'agendaDay':
|
||||
case 'timeGridDay':
|
||||
return 'timeGridDay';
|
||||
case 'week':
|
||||
case 'agendaWeek':
|
||||
case 'timeGridWeek':
|
||||
return 'timeGridWeek';
|
||||
case 'month':
|
||||
case 'agendaMonth':
|
||||
case 'dayGridMonth':
|
||||
default:
|
||||
return 'dayGridMonth';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @return {boolean}
|
||||
@@ -569,7 +605,7 @@ export default class KimaiCalendar {
|
||||
color = apiItem.project.customer.color;
|
||||
}
|
||||
}
|
||||
if (color == null) {
|
||||
if (color === null) {
|
||||
color = defaultColor;
|
||||
}
|
||||
|
||||
@@ -666,8 +702,6 @@ export default class KimaiCalendar {
|
||||
|
||||
/** @type {KimaiAPI} API */
|
||||
const API = this.kimai.getPlugin('api');
|
||||
/** @type {KimaiAlert} ALERT */
|
||||
const ALERT = this.kimai.getPlugin('alert');
|
||||
/** @type {KimaiDateUtils} DATE */
|
||||
const DATES = this.kimai.getPlugin('date');
|
||||
|
||||
@@ -679,13 +713,85 @@ export default class KimaiCalendar {
|
||||
payload.end = null;
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
|
||||
|
||||
const updateUrl = this.options.url.update(event.id);
|
||||
API.patch(updateUrl, JSON.stringify(payload), () => {
|
||||
ALERT.success('action.update.success');
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
}, (error) => {
|
||||
eventArg.revert();
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
API.handleError('action.update.error', error);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* @param {string} view
|
||||
* @param {EventApi[]} events
|
||||
* @private
|
||||
*/
|
||||
_renderDayAndWeekSum(view, events) {
|
||||
if (view === 'dayGridMonth') {
|
||||
// currently we do not display totals in month view
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {KimaiDateUtils} DATES */
|
||||
const DATES = this.kimai.getPlugin('date');
|
||||
|
||||
const durations = {};
|
||||
|
||||
if (view === 'timeGridWeek') {
|
||||
// make sure we have an entry for every day of the week, even days without timesheets
|
||||
document.querySelectorAll(`th.fc-col-header-cell[data-date]`).forEach(cell => {
|
||||
durations[cell.dataset.date] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
events.forEach(item => {
|
||||
const start = DateTime.fromJSDate(item.start).toUTC();
|
||||
const dateStr = start.toFormat('yyyy-MM-dd');
|
||||
|
||||
if (!durations[dateStr]) {
|
||||
durations[dateStr] = 0;
|
||||
}
|
||||
|
||||
// absences or public holidays are all day
|
||||
if (item.end !== null) {
|
||||
const end = DateTime.fromJSDate(item.end).toUTC();
|
||||
const duration = end.diff(start, 'hours').as('seconds');
|
||||
durations[dateStr] += duration;
|
||||
}
|
||||
});
|
||||
|
||||
const dailyTotals = document.querySelectorAll('.fc-dailytotal');
|
||||
dailyTotals.forEach(element => element.remove());
|
||||
for (const dateValue in durations) {
|
||||
const durationValue = durations[dateValue];
|
||||
|
||||
if (view === 'timeGridWeek') { // this is the week view
|
||||
const headerCells = document.querySelectorAll(`th.fc-col-header-cell[data-date="${dateValue}"]`);
|
||||
|
||||
headerCells.forEach(cell => {
|
||||
const newElement = document.createElement('div');
|
||||
newElement.classList.add('fc-dailytotal');
|
||||
newElement.textContent = DATES.formatSeconds(durationValue);
|
||||
cell.appendChild(newElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// this is the day view
|
||||
if (view === 'timeGridDay') {
|
||||
const dayEl = document.querySelector('th.fc-day');
|
||||
const dayDate = dayEl.dataset.date;
|
||||
const dayTotal = document.querySelectorAll('.fc-dailytotal');
|
||||
dayTotal.forEach(element => element.remove());
|
||||
|
||||
const newElement = document.createElement('div');
|
||||
newElement.classList.add('fc-dailytotal');
|
||||
newElement.textContent = DATES.formatSeconds(durations[dayDate]);
|
||||
dayEl.appendChild(newElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export default class KimaiContextMenu {
|
||||
}
|
||||
dropdownElement.removeEventListener('click', dropdownListener);
|
||||
document.removeEventListener('click', dropdownListener);
|
||||
}
|
||||
};
|
||||
|
||||
dropdownElement.addEventListener('click', dropdownListener);
|
||||
document.addEventListener('click', dropdownListener);
|
||||
|
||||
@@ -16,7 +16,6 @@ export default class KimaiPaginatedBoxWidget {
|
||||
constructor(boxId) {
|
||||
this.selector = boxId;
|
||||
const widget = document.querySelector(this.selector);
|
||||
this.href = widget.dataset['href'];
|
||||
|
||||
if (widget.dataset['reload'] !== undefined) {
|
||||
this.events = widget.dataset['reload'].split(' ');
|
||||
@@ -61,7 +60,7 @@ export default class KimaiPaginatedBoxWidget {
|
||||
// and this event will hide it afterwards
|
||||
const hideOverlay = () => {
|
||||
document.dispatchEvent(new Event('kimai.reloadedContent'));
|
||||
}
|
||||
};
|
||||
|
||||
window.kimai.getPlugin('fetch').fetch(url)
|
||||
.then(response => {
|
||||
@@ -93,7 +92,7 @@ export default class KimaiPaginatedBoxWidget {
|
||||
if (node.tagName !== undefined && node.tagName === 'SCRIPT') {
|
||||
const script = document.createElement('script');
|
||||
script.text = node.innerHTML;
|
||||
node.parentNode.replaceChild(script, node );
|
||||
node.parentNode.replaceChild(script, node);
|
||||
} else {
|
||||
for (const child of node.childNodes) {
|
||||
this._makeScriptExecutable(child);
|
||||
|
||||
@@ -5,13 +5,20 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
$modal-backdrop-opacity: 0.6 !default;
|
||||
$modal-backdrop-opacity: 0.7 !default;
|
||||
|
||||
@import "~@tabler/core/scss/config";
|
||||
|
||||
/*
|
||||
THIS DOES NOT WORK AS EXPECTED, AS TABLER IS NOT REGENERATED HERE.
|
||||
SO WE CANNOT CHANGE VARIABLES, LIKE THE ABOVE...
|
||||
*/
|
||||
@import '~@tabler/core/dist/css/tabler.rtl.css';
|
||||
@import '~@tabler/core/dist/css/tabler-themes.rtl.css';
|
||||
|
||||
@import '~tom-select/dist/scss/tom-select';
|
||||
@import '~tom-select/dist/scss/tom-select.bootstrap5';
|
||||
|
||||
@import '~@tabler/core/dist/css/tabler-vendors.rtl.css';
|
||||
|
||||
/**
|
||||
@@ -23,21 +30,6 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts/";
|
||||
@import '~@fortawesome/fontawesome-free/scss/solid';
|
||||
@import '~@fortawesome/fontawesome-free/scss/brands';
|
||||
|
||||
/**
|
||||
* We need that, to compile the rest of the file
|
||||
*/
|
||||
@import "bootstrap/scss/functions";
|
||||
|
||||
@import "@tabler/core/src/scss/variables";
|
||||
@import "@tabler/core/src/scss/variables-dark";
|
||||
@import "@tabler/core/src/scss/mixins";
|
||||
|
||||
@import "bootstrap/scss/variables";
|
||||
@import "bootstrap/scss/variables-dark";
|
||||
@import "bootstrap/scss/maps";
|
||||
@import "bootstrap/scss/mixins";
|
||||
@import "bootstrap/scss/utilities";
|
||||
|
||||
/**
|
||||
* Kimai related stuff
|
||||
*/
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
$modal-backdrop-opacity: 0.6 !default;
|
||||
$modal-backdrop-opacity: 0.7 !default;
|
||||
|
||||
@import '~@tabler/core/src/scss/tabler';
|
||||
@import '~@tabler/core/src/scss/vendor/litepicker';
|
||||
@import '~@tabler/core/src/scss/vendor/tom-select';
|
||||
@import '~@tabler/core/scss/tabler';
|
||||
@import '~@tabler/core/scss/tabler-themes';
|
||||
@import '~@tabler/core/scss/vendor/litepicker';
|
||||
|
||||
@import '~tom-select/dist/scss/tom-select.bootstrap5';
|
||||
@import '~@tabler/core/scss/vendor/tom-select';
|
||||
|
||||
/**
|
||||
* Everyone needs icons
|
||||
|
||||
@@ -5,111 +5,30 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: 10pt;
|
||||
margin: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
table, tr, td, th, p, h1, h2, h3, h4, h5 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
th {
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.text-small {
|
||||
font-size: 7pt;
|
||||
}
|
||||
.padding-left {
|
||||
padding-left: 15px;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-left, .text-begin {
|
||||
text-align: left;
|
||||
}
|
||||
.text-right, .text-end {
|
||||
text-align: right;
|
||||
}
|
||||
.text-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.date {
|
||||
font-size: 80%;
|
||||
text-align: right;
|
||||
padding-top: 6px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
}
|
||||
table.addresses, p {
|
||||
line-height: 14pt;
|
||||
}
|
||||
/* The address table */
|
||||
table.addresses {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
/* The PDF header */
|
||||
table.header {
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
table.header .title {
|
||||
font-weight: normal;
|
||||
font-size: 140%;
|
||||
}
|
||||
/* The PDF footer */
|
||||
table.footer {
|
||||
border-top: 1px solid #999;
|
||||
font-size: 80%;
|
||||
padding-top: 8px;
|
||||
line-height: 14px;
|
||||
}
|
||||
table.footer td {
|
||||
vertical-align: top;
|
||||
}
|
||||
/* The invoice items list */
|
||||
table.items {
|
||||
border-collapse: collapse;
|
||||
margin-top: 50px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
table.items thead th,
|
||||
table.items tbody td {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
table.items thead th.first,
|
||||
table.items tbody td.first {
|
||||
padding-left: 0;
|
||||
}
|
||||
table.items thead th.last,
|
||||
table.items tbody td.last,
|
||||
table.items tfoot td.last {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
table.items td,
|
||||
table.items th {
|
||||
padding: 8px 6px;
|
||||
}
|
||||
table.items tfoot td,
|
||||
table.items tfoot th {
|
||||
padding: 20px 0 0 0;
|
||||
line-height: 1px;
|
||||
}
|
||||
table.items tfoot td.last {
|
||||
padding-left: 10px;
|
||||
}
|
||||
table.items tr.odd {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
body { font-family: sans-serif; font-size: 10pt; margin: 4px; padding: 0; }
|
||||
table, tr, td, th, p, h1, h2, h3, h4, h5 { padding: 0; margin: 0;}
|
||||
th { border: none; text-align: left; }
|
||||
td { vertical-align: top; }
|
||||
p { margin-bottom: 12px; }
|
||||
.text-small { font-size: 7pt; }
|
||||
.padding-left { padding-left: 15px; }
|
||||
.text-center { text-align: center; }
|
||||
.text-left, .text-begin { text-align: left; }
|
||||
.text-right, .text-end { text-align: right; }
|
||||
.text-nowrap { white-space: nowrap; }
|
||||
.date { font-size: 80%; text-align: right; padding-top: 6px; }
|
||||
table { width: 100%; border-spacing: 0; }
|
||||
table.addresses, p { line-height: 14pt; }
|
||||
table.addresses { margin-bottom: 30px; }
|
||||
table.header { padding-bottom: 4px; border-bottom: 1px solid #999; }
|
||||
table.header .title { font-weight: normal; font-size: 140%; }
|
||||
table.footer { border-top: 1px solid #999; font-size: 80%; padding-top: 8px; line-height: 14px; }
|
||||
table.footer td { vertical-align: top; }
|
||||
table.items { border-collapse: collapse; margin-top: 50px; margin-bottom: 60px; }
|
||||
table.items thead th, table.items tbody td { border-bottom: 1px solid #ccc; }
|
||||
table.items thead th.first, table.items tbody td.first { padding-left: 0; }
|
||||
table.items thead th.last, table.items tbody td.last, table.items tfoot td.last { padding-right: 0; }
|
||||
table.items td, table.items th { padding: 8px 6px; }
|
||||
table.items tfoot td, table.items tfoot th { padding: 20px 0 0 0; line-height: 1px; }
|
||||
table.items tfoot td.last { padding-left: 10px; }
|
||||
table.items tr.odd { background-color: #f5f5f5; }
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
@import "modal";
|
||||
@import "progressbar";
|
||||
@import "avatar";
|
||||
@import "theme-dark";
|
||||
@import "pages";
|
||||
@import "weekly-hours";
|
||||
@import "tabler-fixes";
|
||||
@import "help";
|
||||
@import "rtl";
|
||||
|
||||
@@ -52,6 +52,13 @@ table.dataTable thead > tr > th.hw-min {
|
||||
width: 1%;
|
||||
white-space: normal;
|
||||
}
|
||||
@include media-breakpoint-up(sm) {
|
||||
th.w-sm-min,
|
||||
td.w-sm-min {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
/* If a table column contains ONLY avatar <img> it will collapse, so make it a defined width */
|
||||
.w-avatar {
|
||||
width: 40px;
|
||||
@@ -86,3 +93,19 @@ table.dataTable thead > tr > th.hw-min {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
blockquote {
|
||||
p {
|
||||
/* Parsedown wraps content in a <p> like <blockquote><p> */
|
||||
margin: 0;
|
||||
}
|
||||
/* Tabler doesn't style blockquotes */
|
||||
border-left: 15px var(--tblr-border-style) var(--tblr-border-color);
|
||||
background-color: var(--tblr-bg-surface-secondary);
|
||||
}
|
||||
pre code.hljs {
|
||||
padding: 0;
|
||||
background-color: var(--tblr-bg-surface-dark);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.input-group {
|
||||
width: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +36,6 @@
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
border-radius: var(--tblr-border-radius);
|
||||
}
|
||||
|
||||
@@ -53,4 +55,19 @@ fieldset > .mb-3.row:last-child {
|
||||
|
||||
.form-fieldset-light:last-child {
|
||||
border:none;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
.dropdown-action {
|
||||
visibility: hidden;
|
||||
opacity: 0.5;
|
||||
}
|
||||
&:hover {
|
||||
.dropdown-action {
|
||||
visibility: visible;
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
.inline-search {
|
||||
max-width: 200px;
|
||||
/* the entire inline-search should actually use .input-group-flat, but that causes some weird css problems inside the dropdown menu */
|
||||
#searchTerm {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 360px) {
|
||||
|
||||
@@ -12,14 +12,6 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1021;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
> .fas {
|
||||
position: absolute;
|
||||
top: 50vh;
|
||||
left: 50vw;
|
||||
margin-left: -15px;
|
||||
margin-top: -15px;
|
||||
color: #000;
|
||||
font-size: 30px;
|
||||
}
|
||||
/*background-color: var(--tblr-backdrop-bg);*/
|
||||
opacity: $modal-backdrop-opacity;
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
----------- This file contains rules that only page to certain pages -----------
|
||||
*/
|
||||
|
||||
/* weekly working times AKA quick-entries form */
|
||||
section.quick-entry-page {
|
||||
|
||||
#quick_entry_form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* shrinks the dropdown elements for project and activity to fit into the table, instead of adjusting to the longest option element */
|
||||
.ts-wrapper {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/* make the duration field and dropdown smaller */
|
||||
#quick_entry_box {
|
||||
.duration-widget {
|
||||
.input-group {
|
||||
min-width: 85px;
|
||||
}
|
||||
.btn-duration-preset {
|
||||
padding: 7px 8px 7px 5px;
|
||||
}
|
||||
input {
|
||||
padding: 7px 3px 7px 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,11 @@
|
||||
.content-wrapper {
|
||||
margin: 0!important;
|
||||
}
|
||||
section.content {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
span i.dot {
|
||||
display: none;
|
||||
margin-right: 0!important;
|
||||
}
|
||||
.float-help {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
|
||||
/* bootstrap thead.sticky-top has z-index 1020 and tomselect dropdowns often hide behind that */
|
||||
/*
|
||||
1. bootstrap thead.sticky-top has z-index 1020 and tomselect dropdowns often hide behind that: use 1021
|
||||
2. bootstrap modal uses 1055, tomselect with dropdownParent: 'body' (required for use in table.responsive, e.g quick.entry on small devices) needs 1056, so it works in modals
|
||||
*/
|
||||
.ts-dropdown {
|
||||
z-index: 1021;
|
||||
z-index: 1056;
|
||||
|
||||
}
|
||||
|
||||
/* Tabler defines a min-width of 7rem, what makes many input fields become a two-line field
|
||||
|
||||
@@ -4,7 +4,7 @@ fieldset legend.col-form-label {
|
||||
}
|
||||
|
||||
/* See project assignment to teams */
|
||||
fieldset.form-fieldset legend {
|
||||
fieldset.form-fieldset > legend {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -17,18 +17,43 @@ fieldset.form-fieldset legend {
|
||||
--tblr-dropdown-divider-margin-y: .5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
fixes translucent scroll border, e.g. in timesheet modal duration dropdown
|
||||
https://github.com/tabler/tabler/issues/1606
|
||||
*/
|
||||
.dropdown-menu {
|
||||
background-clip: border-box;
|
||||
:host,:root,[data-bs-theme=light] {
|
||||
/* remove the shadow from all form element groups */
|
||||
--tblr-box-shadow-input: none;
|
||||
}
|
||||
|
||||
/*
|
||||
hide empty fieldset
|
||||
https://github.com/tabler/tabler/issues/1650
|
||||
*/
|
||||
fieldset:empty {
|
||||
display: none;
|
||||
.navbar[data-bs-theme=dark] {
|
||||
/* give navigation elements slightly higher contrast */
|
||||
--tblr-navbar-color: hsla(0,0%,100%,.8);
|
||||
}
|
||||
|
||||
/* this is not a bugfix, but it adds a border for better visual segregation */
|
||||
.nav-pills .nav-link.active {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
/* fixes contrast of batch-update checkboxes - https://github.com/kimai/kimai/issues/5146 */
|
||||
.multiupdater[type=checkbox] {
|
||||
--tblr-border-color-translucent: rgba(4, 32, 69, .2);
|
||||
}
|
||||
|
||||
/* fixes height for long tags https://github.com/kimai/kimai/issues/5169 */
|
||||
.tag {
|
||||
--tblr-tag-height: unset;
|
||||
}
|
||||
|
||||
/* Page title (e.g. Dashboard) does not work properly in navbar, just outside */
|
||||
.navbar {
|
||||
.page-title {
|
||||
color: var(--tblr-body-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Highlighted text is not visible - https://github.com/tabler/tabler/issues/2603 */
|
||||
[data-bs-theme=dark] {
|
||||
::selection,
|
||||
.text-selected {
|
||||
background-color: var(--#{$prefix}primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,22 +17,18 @@ table.dataTable {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
thead .sorting_asc,
|
||||
thead .sorting_desc {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
thead .sorting:after,
|
||||
thead .sorting_asc:after,
|
||||
thead .sorting_desc:after {
|
||||
padding-left: 5px;
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-family: 'Font Awesome\ 6 Free';
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
font-weight: 700
|
||||
}
|
||||
thead .sorting:after {
|
||||
opacity: 0.2;
|
||||
content: "\f0dc"; /* sort */
|
||||
content: "\f0dc";
|
||||
}
|
||||
thead .sorting_asc:after {
|
||||
content: "\f077";
|
||||
@@ -42,7 +38,7 @@ table.dataTable {
|
||||
}
|
||||
|
||||
thead > tr > th {
|
||||
vertical-align: top;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -92,13 +88,13 @@ table.dataTable {
|
||||
&.summary td {
|
||||
font-weight: bold;
|
||||
border: 0;
|
||||
background-color: var(--tblr-bg-surface-secondary);
|
||||
background-color: var(--tblr-secondary-lt);
|
||||
}
|
||||
th.multiCheckbox {
|
||||
width: 15px;
|
||||
}
|
||||
&.overlapping {
|
||||
border-top: 2px solid rgba(214,57,57,.1);
|
||||
border-top: 2px solid rgba(214,57,57,.2);
|
||||
}
|
||||
&.exported {
|
||||
opacity: 0.7;
|
||||
@@ -123,7 +119,7 @@ table.dataTable {
|
||||
}
|
||||
th.weekend,
|
||||
td.weekend {
|
||||
background-color: var(--tblr-bg-surface-secondary);
|
||||
background-color: var(--tblr-bg-surface-tertiary);
|
||||
}
|
||||
/* order is important, "today” should overwrite "weekend" therefor later in the file */
|
||||
th.today {
|
||||
@@ -135,14 +131,6 @@ table.dataTable {
|
||||
}
|
||||
}
|
||||
|
||||
/* Quick entry form */
|
||||
.form-dataTable {
|
||||
table.dataTable {
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.table-hover {
|
||||
tr {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
@include color-mode(dark, true) {
|
||||
table.dataTable {
|
||||
th.today {
|
||||
color: var(--tblr-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
|
||||
> .fas {
|
||||
color: var(--tblr-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
178
assets/sass/weekly-hours.scss
Normal file
178
assets/sass/weekly-hours.scss
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* This file contains rules for the "weekly-hours" AKA "quick-entries" form */
|
||||
|
||||
.quick-entry-page {
|
||||
|
||||
.form-dataTable {
|
||||
table.dataTable {
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#quick_entry_form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ts-wrapper {
|
||||
min-height: 36px;
|
||||
max-height: 36px;
|
||||
max-width: 150px;
|
||||
|
||||
.ts-control {
|
||||
white-space: nowrap;
|
||||
.item {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table tbody td:not(.total) {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.duration-widget input.duration-input {
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
/* make the duration field and dropdown smaller */
|
||||
#quick_entry_box {
|
||||
.duration-widget {
|
||||
.input-group {
|
||||
max-width: 50px;
|
||||
min-width: 50px;
|
||||
}
|
||||
.btn-duration-preset {
|
||||
display: none;
|
||||
min-width: 30px;
|
||||
}
|
||||
input.duration-input {
|
||||
padding: 7px 5px;
|
||||
max-width: 50px;
|
||||
border-bottom-right-radius: var(--tblr-border-radius);
|
||||
border-top-right-radius: var(--tblr-border-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* make sure dropdown never hides behind table responsive */
|
||||
.dropdown-menu {
|
||||
z-index: 1056;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1100px) {
|
||||
.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 165px;
|
||||
}
|
||||
.duration-widget {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1250px) {
|
||||
.quick-entry-page {
|
||||
#quick_entry_box {
|
||||
.duration-widget {
|
||||
.input-group {
|
||||
max-width: 85px;
|
||||
min-width: 75px;
|
||||
}
|
||||
.btn-duration-preset {
|
||||
display: flex;
|
||||
}
|
||||
input.duration-input {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1300px) {
|
||||
.quick-entry-page {
|
||||
#quick_entry_box {
|
||||
.duration-widget {
|
||||
input.duration-input {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ts-wrapper {
|
||||
max-width: 190px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 235px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1500px) {
|
||||
.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 290px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 340px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1800px) {
|
||||
.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 450px;
|
||||
}
|
||||
#quick_entry_box {
|
||||
.duration-widget {
|
||||
.input-group {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
module.exports = {
|
||||
"sourceType": "unambiguous",
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false,
|
||||
"targets": {},
|
||||
"useBuiltIns": "usage",
|
||||
"corejs": 3
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-syntax-dynamic-import"
|
||||
]
|
||||
}
|
||||
@@ -6,8 +6,8 @@ use App\ConsoleApplication;
|
||||
|
||||
set_time_limit(0);
|
||||
|
||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
112
composer.json
112
composer.json
@@ -14,7 +14,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "8.1.*||8.2.*||8.3.*",
|
||||
"php": "8.4.*||8.5.*",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
@@ -28,49 +28,55 @@
|
||||
"azuyalabs/yasumi": "^2.6",
|
||||
"composer/semver": "^3.3",
|
||||
"doctrine/doctrine-bundle": "^2.7",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
"doctrine/orm": "^2.8",
|
||||
"endroid/qr-code": "^4.8",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||
"doctrine/orm": "^3.0",
|
||||
"easybill/zugferd-php": "^2.1",
|
||||
"endroid/qr-code": "^6.0",
|
||||
"friendsofsymfony/rest-bundle": "3.9.0-beta1",
|
||||
"gedmo/doctrine-extensions": "^3.6",
|
||||
"horstoeko/zugferd": "^1.0",
|
||||
"horstoeko/zugferdublbridge": "^1.0",
|
||||
"jms/serializer-bundle": "^5.0",
|
||||
"kevinpapst/tabler-bundle": "^1.2",
|
||||
"kevinpapst/tabler-bundle": "^2.0",
|
||||
"league/csv": "^9.4",
|
||||
"mpdf/mpdf": "^8.0",
|
||||
"nelmio/api-doc-bundle": "^4.0",
|
||||
"nelmio/api-doc-bundle": "^5.0",
|
||||
"nelmio/cors-bundle": "^2.0",
|
||||
"onelogin/php-saml": "^4.0",
|
||||
"pagerfanta/pagerfanta": "^3.0",
|
||||
"phpoffice/phpspreadsheet": "^1.16",
|
||||
"openspout/openspout": "^4.0",
|
||||
"pagerfanta/pagerfanta": "^4.0",
|
||||
"parsedown/parsedown": "^1.6",
|
||||
"phpoffice/phpspreadsheet": "^2.0",
|
||||
"phpoffice/phpword": "^1.0",
|
||||
"psr/container": "^2.0",
|
||||
"psr/log": "^3.0",
|
||||
"scheb/2fa-backup-code": "^6.2",
|
||||
"scheb/2fa-bundle": "^6.2",
|
||||
"scheb/2fa-totp": "^6.2",
|
||||
"symfony/asset": "^6.0",
|
||||
"symfony/console": "^6.0",
|
||||
"symfony/dotenv": "^6.0",
|
||||
"symfony/expression-language": "^6.0",
|
||||
"scheb/2fa-backup-code": "^7.11",
|
||||
"scheb/2fa-bundle": "^7.11",
|
||||
"scheb/2fa-totp": "^7.11",
|
||||
"symfony/asset": "^7.0",
|
||||
"symfony/console": "^7.0",
|
||||
"symfony/dependency-injection": "^7.0",
|
||||
"symfony/dotenv": "^7.0",
|
||||
"symfony/expression-language": "^7.0",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/form": "^6.0",
|
||||
"symfony/framework-bundle": "^6.0",
|
||||
"symfony/http-client": "^6.0",
|
||||
"symfony/intl": "^6.0",
|
||||
"symfony/mailer": "^6.0",
|
||||
"symfony/mime": "^6.0",
|
||||
"symfony/form": "^7.0",
|
||||
"symfony/framework-bundle": "^7.0",
|
||||
"symfony/http-client": "^7.0",
|
||||
"symfony/intl": "^7.0",
|
||||
"symfony/mailer": "^7.0",
|
||||
"symfony/mime": "^7.0",
|
||||
"symfony/monolog-bundle": "^3.4",
|
||||
"symfony/rate-limiter": "^6.0",
|
||||
"symfony/runtime": "^6.0",
|
||||
"symfony/security-bundle": "^6.0",
|
||||
"symfony/security-csrf": "^6.0",
|
||||
"symfony/serializer": "^6.0",
|
||||
"symfony/translation": "^6.0",
|
||||
"symfony/twig-bundle": "^6.0",
|
||||
"symfony/validator": "^6.0",
|
||||
"symfony/process": "^7.0",
|
||||
"symfony/rate-limiter": "^7.0",
|
||||
"symfony/runtime": "^7.0",
|
||||
"symfony/security-bundle": "^7.0",
|
||||
"symfony/security-csrf": "^7.0",
|
||||
"symfony/serializer": "^7.0",
|
||||
"symfony/translation": "^7.0",
|
||||
"symfony/twig-bundle": "^7.0",
|
||||
"symfony/validator": "^7.0",
|
||||
"symfony/webpack-encore-bundle": "^2.0",
|
||||
"symfony/yaml": "^6.0",
|
||||
"symfony/yaml": "^7.0",
|
||||
"twig/cssinliner-extra": "^3.0",
|
||||
"twig/extra-bundle": "^3.0",
|
||||
"twig/inky-extra": "^3.0",
|
||||
@@ -83,21 +89,21 @@
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.2",
|
||||
"fakerphp/faker": "^1.15",
|
||||
"friendsofphp/php-cs-fixer": "^3.3",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0",
|
||||
"phpstan/phpstan-doctrine": "^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.0",
|
||||
"phpstan/phpstan-symfony": "^1.0",
|
||||
"phpunit/phpunit": "9.5.*",
|
||||
"symfony/browser-kit": "^6.0",
|
||||
"symfony/css-selector": "^6.0",
|
||||
"symfony/debug-bundle": "^6.0",
|
||||
"symfony/dom-crawler": "^6.0",
|
||||
"symfony/phpunit-bridge": "^6.0",
|
||||
"symfony/stopwatch": "^6.0",
|
||||
"symfony/var-dumper": "^6.0",
|
||||
"symfony/web-profiler-bundle": "^6.0"
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpstan/phpstan-doctrine": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpstan/phpstan-symfony": "^2.0",
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"symfony/browser-kit": "^7.0",
|
||||
"symfony/css-selector": "^7.0",
|
||||
"symfony/debug-bundle": "^7.0",
|
||||
"symfony/dom-crawler": "^7.0",
|
||||
"symfony/phpunit-bridge": "^7.0",
|
||||
"symfony/stopwatch": "^7.0",
|
||||
"symfony/var-dumper": "^7.0",
|
||||
"symfony/web-profiler-bundle": "^7.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
@@ -112,7 +118,7 @@
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"platform": {
|
||||
"php": "8.1.3"
|
||||
"php": "8.4.4"
|
||||
},
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
@@ -134,7 +140,14 @@
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-mbstring": "*",
|
||||
"symfony/polyfill-intl": "*",
|
||||
"symfony/polyfill-intl-icu": "*",
|
||||
"symfony/polyfill-intl-grapheme": "*",
|
||||
"symfony/polyfill-intl-idn": "*",
|
||||
"symfony/polyfill-intl-normalizer": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php84": "*",
|
||||
"symfony/polyfill-php83": "*",
|
||||
"symfony/polyfill-php82": "*",
|
||||
"symfony/polyfill-php81": "*",
|
||||
"symfony/polyfill-php80": "*",
|
||||
"symfony/polyfill-php74": "*",
|
||||
@@ -171,7 +184,6 @@
|
||||
"@tests-integration"
|
||||
],
|
||||
"linting": [
|
||||
"composer validate --strict",
|
||||
"bin/console lint:container",
|
||||
"bin/console lint:yaml config --parse-tags",
|
||||
"bin/console lint:twig templates --show-deprecations",
|
||||
@@ -201,7 +213,7 @@
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": true,
|
||||
"require": "6.4.*"
|
||||
"require": "7.4.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6751
composer.lock
generated
6751
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ return [
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||
JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
|
||||
FOS\RestBundle\FOSRestBundle::class => ['all' => true],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
||||
packages:
|
||||
avatars:
|
||||
base_path: 'avatars'
|
||||
@@ -1,17 +0,0 @@
|
||||
framework:
|
||||
cache:
|
||||
#app: cache.adapter.redis
|
||||
#default_redis_provider: redis://127.0.0.1:6379
|
||||
|
||||
#app: cache.adapter.memcached
|
||||
#default_memcached_provider: 'memcached://localhost'
|
||||
|
||||
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||
#app: cache.adapter.apcu
|
||||
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
5
config/packages/dama_doctrine_test_bundle.yaml
Normal file
5
config/packages/dama_doctrine_test_bundle.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
when@test:
|
||||
dama_doctrine_test:
|
||||
enable_static_connection: true
|
||||
enable_static_meta_data_cache: true
|
||||
enable_static_query_cache: true
|
||||
5
config/packages/debug.yaml
Normal file
5
config/packages/debug.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
when@dev:
|
||||
debug:
|
||||
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
|
||||
# See the "server:dump" command to start a new server.
|
||||
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
|
||||
@@ -10,27 +10,31 @@ doctrine:
|
||||
default_connection: default
|
||||
connections:
|
||||
default:
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
use_savepoints: true
|
||||
# existing migrations will fail if the schema filter is activated
|
||||
#schema_filter: ~^(?!(bundle_migration_|kimai2_sessions))~
|
||||
url: '%env(DATABASE_URL)%'
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
driver: 'pdo_mysql'
|
||||
charset: utf8mb4
|
||||
default_table_options:
|
||||
charset: utf8mb4
|
||||
collation: utf8mb4_unicode_ci
|
||||
schema_manager_factory: doctrine.dbal.default_schema_manager_factory
|
||||
|
||||
types:
|
||||
datetime: App\Doctrine\UTCDateTimeType
|
||||
datetime_immutable: App\Doctrine\UTCDateTimeImmutableType
|
||||
orm:
|
||||
controller_resolver:
|
||||
auto_mapping: true
|
||||
auto_generate_proxy_classes: '%kernel.debug%'
|
||||
auto_mapping: false
|
||||
auto_generate_proxy_classes: true
|
||||
default_entity_manager: default
|
||||
enable_lazy_ghost_objects: true
|
||||
enable_native_lazy_objects: true
|
||||
entity_managers:
|
||||
default:
|
||||
identity_generation_preferences:
|
||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||
validate_xml_mapping: true
|
||||
report_fields_where_declared: true
|
||||
connection: default
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
@@ -54,17 +58,23 @@ when@test:
|
||||
connections:
|
||||
default:
|
||||
logging: false
|
||||
use_savepoints: true
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
orm:
|
||||
metadata_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
auto_generate_proxy_classes: false
|
||||
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
doctrine_migrations:
|
||||
migrations_paths:
|
||||
# namespace is arbitrary but should be different from App\Migrations
|
||||
# as migrations classes should NOT be autoloaded
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
enable_profiler: false
|
||||
transactional: false
|
||||
storage:
|
||||
table_storage:
|
||||
table_name: 'migration_versions'
|
||||
migrations_paths:
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
custom_template: '%kernel.project_dir%/migrations/MigrationTemplate.txt'
|
||||
custom_template: '%kernel.project_dir%/migrations/MigrationTemplate.txt'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Read the documentation: https://fosrestbundle.readthedocs.io/en/3.x/
|
||||
fos_rest:
|
||||
param_fetcher_listener:
|
||||
enabled: true
|
||||
@@ -39,3 +40,5 @@ fos_rest:
|
||||
- { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json ] }
|
||||
zone:
|
||||
- { path: ^/api/* }
|
||||
service:
|
||||
view_handler: App\API\ViewHandler
|
||||
@@ -1,17 +1,21 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
parameters:
|
||||
env(TRUSTED_PROXIES): ''
|
||||
env(TRUSTED_HOSTS): ''
|
||||
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
csrf_protection: true
|
||||
annotations: false
|
||||
handle_all_throwables: true
|
||||
disallow_search_engine_index: true
|
||||
set_content_language_from_locale: true
|
||||
|
||||
serializer:
|
||||
enable_attributes: true
|
||||
|
||||
trusted_proxies: '%env(string:TRUSTED_PROXIES)%'
|
||||
trusted_hosts: '%env(string:TRUSTED_HOSTS)%'
|
||||
|
||||
exceptions:
|
||||
App\Validator\ValidationFailedException:
|
||||
@@ -23,6 +27,7 @@ framework:
|
||||
# Enables session support. Note that the session will ONLY be started if you read or write from it.
|
||||
# Remove or comment this section to explicitly disable session support.
|
||||
session:
|
||||
name: KIMAI_SESSION
|
||||
handler_id: App\Security\SessionHandler
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
@@ -32,6 +37,9 @@ framework:
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
||||
|
||||
mailer:
|
||||
dsn: '%env(MAILER_URL)%'
|
||||
|
||||
|
||||
@@ -3,10 +3,6 @@ jms_serializer:
|
||||
datetime:
|
||||
default_format: 'Y-m-d\TH:i:sO' # DATE_ISO8601
|
||||
visitors:
|
||||
json_serialization:
|
||||
options:
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
xml_serialization:
|
||||
format_output: '%kernel.debug%'
|
||||
metadata:
|
||||
@@ -18,3 +14,20 @@ jms_serializer:
|
||||
excluded: []
|
||||
property_naming:
|
||||
id: 'jms_serializer.identical_property_naming_strategy'
|
||||
|
||||
when@prod:
|
||||
jms_serializer:
|
||||
visitors:
|
||||
json_serialization:
|
||||
options:
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
|
||||
when@dev:
|
||||
jms_serializer:
|
||||
visitors:
|
||||
json_serialization:
|
||||
options:
|
||||
- JSON_PRETTY_PRINT
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
|
||||
@@ -105,7 +105,7 @@ kimai:
|
||||
TEAMS: ['view_team','create_team','edit_team','delete_team']
|
||||
LOCKDOWN: ['lockdown_grace_timesheet','lockdown_override_timesheet']
|
||||
REPORTING: ['view_reporting','view_other_reporting','project_reporting','customer_reporting']
|
||||
EVERYONE: ['api_access']
|
||||
EVERYONE: ['api_access','hours_own_profile']
|
||||
# permissions which are deactivated, as these features are hidden for now
|
||||
# brave users can try to activate them and be surprised what happens
|
||||
REGISTER_BETA: []
|
||||
@@ -119,8 +119,8 @@ kimai:
|
||||
roles:
|
||||
ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting']
|
||||
ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member','hours_other_profile']
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile','hours_other_profile']
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile','hours_other_profile']
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile','hours_other_profile','create_export_template']
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile','hours_other_profile','create_export_template']
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -1,39 +1,25 @@
|
||||
monolog:
|
||||
channels: ["deprecation"]
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: notice
|
||||
handler: nested
|
||||
excluded_http_codes: [403, 404]
|
||||
channels: ["!deprecation"]
|
||||
nested:
|
||||
type: stream
|
||||
level: info
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
formatter: monolog.formatter.kimai
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!deprecation"]
|
||||
# TODO reactivate once the many Doctrine deprecations are fixed
|
||||
# deprecation:
|
||||
# type: stream
|
||||
# channels: ["deprecation"]
|
||||
# path: "%kernel.logs_dir%/deprecations.log"
|
||||
# formatter: monolog.formatter.deprecation
|
||||
channels:
|
||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: notice
|
||||
handler: file_log
|
||||
excluded_http_codes:
|
||||
- {400: ['^/api/']}
|
||||
- {401: ['^/api/']}
|
||||
- 403
|
||||
- 404
|
||||
- 405
|
||||
channels: ["!event", "!deprecation"]
|
||||
file_log:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event", "!deprecation"]
|
||||
formatter: monolog.formatter.kimai
|
||||
console:
|
||||
type: console
|
||||
@@ -49,8 +35,37 @@ when@test:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: info
|
||||
channels: ["!event"]
|
||||
level: debug
|
||||
formatter: monolog.formatter.kimai
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: file_log
|
||||
excluded_http_codes:
|
||||
- {400: ['^/api/']}
|
||||
- {401: ['^/api/']}
|
||||
- 403
|
||||
- 404
|
||||
- {405: ['/homepage$', '/login_check$', '/export/data$']}
|
||||
channels: ["!deprecation"]
|
||||
file_log:
|
||||
type: stream
|
||||
level: info
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
formatter: monolog.formatter.kimai
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!deprecation"]
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
nelmio_api_doc:
|
||||
operation_id_generation: conditionally_prepend
|
||||
models:
|
||||
use_jms: true
|
||||
names:
|
||||
- { alias: CustomerEditForm, type: App\Form\API\CustomerApiEditForm, groups: [Default, Entity, Customer] }
|
||||
- { alias: CustomerEditForm, type: App\Form\API\CustomerApiEditForm, groups: [Default, Entity, Customer, Customer_Entity] }
|
||||
- { alias: CustomerEntity, type: App\Entity\Customer, groups: [Default, Entity, Customer, Customer_Entity, Not_Expanded] }
|
||||
- { alias: Customer, type: App\Entity\Customer, groups: [Default, Not_Expanded] }
|
||||
- { alias: CustomerRate, type: App\Entity\CustomerRate, groups: [Default, Entity, Customer_Rate] }
|
||||
- { alias: CustomerRateForm, type: App\Form\API\CustomerRateApiForm, groups: [Default, Entity, Customer_Rate] }
|
||||
- { alias: CustomerCollection, type: App\Entity\Customer, groups: [Default, Collection, Customer] }
|
||||
- { alias: ProjectEditForm, type: App\Form\API\ProjectApiEditForm, groups: [Default, Entity, Project] }
|
||||
- { alias: ProjectEditForm, type: App\Form\API\ProjectApiEditForm, groups: [Default, Entity, Project, Project_Entity] }
|
||||
- { alias: ProjectEntity, type: App\Entity\Project, groups: [Default, Entity, Project, Project_Entity, Not_Expanded] }
|
||||
- { alias: Project, type: App\Entity\Project, groups: [Default, Not_Expanded] }
|
||||
- { alias: ProjectExpanded, type: App\Entity\Project, groups: [Default, Expanded] }
|
||||
- { alias: ProjectRate, type: App\Entity\ProjectRate, groups: [Default, Entity, Project_Rate] }
|
||||
- { alias: ProjectRateForm, type: App\Form\API\ProjectRateApiForm, groups: [Default, Entity, Project_Rate] }
|
||||
- { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] }
|
||||
- { alias: ActivityEditForm, type: App\Form\API\ActivityApiEditForm, groups: [Default, Entity, Activity] }
|
||||
- { alias: ActivityEditForm, type: App\Form\API\ActivityApiEditForm, groups: [Default, Entity, Activity, Activity_Entity] }
|
||||
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity, Activity_Entity, Not_Expanded] }
|
||||
- { alias: Activity, type: App\Entity\Activity, groups: [Default, Not_Expanded] }
|
||||
- { alias: ActivityExpanded, type: App\Entity\Activity, groups: [Default, Expanded] }
|
||||
@@ -28,7 +29,7 @@ nelmio_api_doc:
|
||||
- { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet, Timesheet_Entity, Not_Expanded] }
|
||||
- { alias: TimesheetExpanded, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet, Timesheet_Entity, Expanded] }
|
||||
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Not_Expanded] }
|
||||
- { alias: TimesheetCollectionExpanded, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Subresource, Expanded] }
|
||||
- { alias: TimesheetCollectionExpanded, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Expanded] }
|
||||
- { alias: UserCreateForm, type: App\Form\API\UserApiCreateForm, groups: [Default, Entity, User, User_Entity] }
|
||||
- { alias: UserEditForm, type: App\Form\API\UserApiEditForm, groups: [Default, Entity, User, User_Entity] }
|
||||
- { alias: User, type: App\Entity\User, groups: [Default] }
|
||||
@@ -40,21 +41,31 @@ nelmio_api_doc:
|
||||
- { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] }
|
||||
- { alias: TeamMember, type: App\Entity\TeamMember, groups: [Team_Entity] }
|
||||
- { alias: TeamMembership, type: App\Entity\TeamMember, groups: [User_Entity] }
|
||||
- { alias: Invoice, type: App\Entity\Invoice, groups: [Default, Entity, Invoice, Invoice_Entity] }
|
||||
- { alias: InvoiceCollection, type: App\Entity\Invoice, groups: [Default, Collection, Invoice] }
|
||||
areas:
|
||||
path_patterns:
|
||||
- ^/api(?!/doc)
|
||||
default:
|
||||
path_patterns:
|
||||
- ^/api(?!/doc)
|
||||
security:
|
||||
bearer:
|
||||
type: 'http'
|
||||
description: 'API Token'
|
||||
bearerFormat: 'KIMAI'
|
||||
scheme: 'bearer'
|
||||
documentation:
|
||||
info:
|
||||
title: Kimai - API Docs
|
||||
title: Kimai - API
|
||||
description: |
|
||||
JSON API for the Kimai time-tracking software: [API documentation](https://www.kimai.org/documentation/rest-api.html), [Swagger definition file](doc.json)
|
||||
version: '1.0'
|
||||
components:
|
||||
securitySchemes:
|
||||
bearer:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: KIMAI
|
||||
description: API Token
|
||||
JSON API for the Kimai time-tracking software. Find more infos in our [API documentation](https://www.kimai.org/documentation/rest-api.html).
|
||||
version: '1.1'
|
||||
security:
|
||||
- bearer: []
|
||||
html_config:
|
||||
# assets_mode: cdn
|
||||
# https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
|
||||
swagger_ui_config: []
|
||||
# https://redocly.com/docs/redoc/config/
|
||||
redocly_config: []
|
||||
# https://docs.stoplight.io/docs/elements/b074dc47b2826-elements-configuration-options
|
||||
stoplight_config: { basePath: '/api/doc', router: 'memory', logo: '/touch-icon-192x192.png', hideInternal: true }
|
||||
|
||||
@@ -3,12 +3,7 @@ parameters:
|
||||
|
||||
nelmio_cors:
|
||||
defaults:
|
||||
allow_credentials: false
|
||||
# allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
# allow_headers: ['Content-Type', 'Authorization', 'X-AUTH-USER', 'X-AUTH-TOKEN']
|
||||
# allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
# expose_headers: ['Link']
|
||||
# max_age: 3600
|
||||
# for security reasons we do not allow CORS by default
|
||||
allow_origin: []
|
||||
allow_headers: []
|
||||
allow_methods: []
|
||||
@@ -18,8 +13,17 @@ nelmio_cors:
|
||||
origin_regex: true
|
||||
forced_allow_origin_value: ~
|
||||
paths:
|
||||
# only the API endpoints are accessible
|
||||
'^/api/':
|
||||
# allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
allow_origin: ['*']
|
||||
allow_headers: ['X-AUTH-USER', 'X-AUTH-TOKEN', 'Content-Type']
|
||||
allow_headers: ['Content-Type', 'Authorization']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
# expose_headers: ['Link']
|
||||
max_age: 3600
|
||||
|
||||
when@dev:
|
||||
nelmio_cors:
|
||||
paths:
|
||||
'^/api/':
|
||||
allow_private_network: true
|
||||
|
||||
@@ -5,3 +5,17 @@ framework:
|
||||
limit: 250
|
||||
interval: '1 hour'
|
||||
lock_factory: null
|
||||
reset_password:
|
||||
policy: 'fixed_window'
|
||||
limit: 10
|
||||
interval: '1 hour'
|
||||
lock_factory: null
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
rate_limiter:
|
||||
reset_password:
|
||||
policy: 'fixed_window'
|
||||
limit: 100
|
||||
interval: '1 minute'
|
||||
lock_factory: null
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
parameters:
|
||||
env(default_uri): 'http://localhost'
|
||||
|
||||
framework:
|
||||
router:
|
||||
utf8: true
|
||||
@@ -5,7 +8,7 @@ framework:
|
||||
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
#default_uri: http://localhost
|
||||
default_uri: '%env(DEFAULT_URI)%'
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
App\Entity\User: auto
|
||||
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
chain_provider:
|
||||
chain:
|
||||
@@ -11,12 +12,11 @@ security:
|
||||
class: App\Entity\User
|
||||
kimai_ldap:
|
||||
id: App\Ldap\LdapUserProvider
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
# Ensure dev tools and static assets are always allowed
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
|
||||
api:
|
||||
access_token:
|
||||
token_handler: App\API\Authentication\AccessTokenHandler
|
||||
@@ -27,84 +27,70 @@ security:
|
||||
stateless: true
|
||||
remember_me: false
|
||||
provider: chain_provider
|
||||
custom_authenticators:
|
||||
- App\API\Authentication\TokenAuthenticator
|
||||
|
||||
secured_area:
|
||||
kimai_ldap: ~
|
||||
pattern: ^/
|
||||
user_checker: App\Security\UserChecker
|
||||
stateless: false
|
||||
|
||||
entry_point: form_login
|
||||
|
||||
custom_authenticators:
|
||||
- App\Saml\SamlAuthenticator
|
||||
|
||||
remember_me:
|
||||
name: KIMAI_REMEMBER
|
||||
secret: '%kernel.secret%'
|
||||
lifetime: 604800
|
||||
path: /
|
||||
always_remember_me: true
|
||||
|
||||
# activate all configured user provider
|
||||
provider: chain_provider
|
||||
|
||||
form_login:
|
||||
check_path: security_check
|
||||
login_path: login
|
||||
enable_csrf: true
|
||||
|
||||
two_factor:
|
||||
auth_form_path: 2fa_login
|
||||
check_path: 2fa_login_check
|
||||
remember_me_sets_trusted: true
|
||||
|
||||
logout:
|
||||
path: logout
|
||||
target: homepage
|
||||
enable_csrf: false
|
||||
|
||||
login_throttling:
|
||||
max_attempts: 5
|
||||
interval: '5 minutes'
|
||||
|
||||
login_link:
|
||||
check_route: link_login_check
|
||||
signature_properties: ['id']
|
||||
lifetime: 900
|
||||
max_uses: 3
|
||||
|
||||
access_decision_manager:
|
||||
# only grants access if there is no voter denying access
|
||||
strategy: unanimous
|
||||
allow_if_all_abstain: false
|
||||
|
||||
role_hierarchy:
|
||||
ROLE_USER: ~
|
||||
ROLE_TEAMLEAD: ROLE_USER
|
||||
ROLE_ADMIN: ROLE_TEAMLEAD
|
||||
ROLE_USER: ~
|
||||
ROLE_TEAMLEAD: ROLE_USER
|
||||
ROLE_ADMIN: ROLE_TEAMLEAD
|
||||
ROLE_SUPER_ADMIN: ROLE_ADMIN
|
||||
|
||||
# Note: Only the *first* matching rule is applied
|
||||
access_control:
|
||||
- { path: '^/auth/2fa', role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
|
||||
- { path: '^/auth', roles: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)$', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/auth', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/login', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/register', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/resetting', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/', roles: ROLE_USER }
|
||||
- { path: '^/api', roles: IS_AUTHENTICATED }
|
||||
- {path: '^/auth/2fa', role: IS_AUTHENTICATED_2FA_IN_PROGRESS}
|
||||
- {path: '^/auth', roles: PUBLIC_ACCESS}
|
||||
- {path: '^/{_locale}$', role: PUBLIC_ACCESS}
|
||||
- {path: '^/{_locale}/auth', role: PUBLIC_ACCESS}
|
||||
- {path: '^/{_locale}/login', role: PUBLIC_ACCESS}
|
||||
- {path: '^/{_locale}/register', role: PUBLIC_ACCESS}
|
||||
- {path: '^/{_locale}/resetting', role: PUBLIC_ACCESS}
|
||||
- {path: '^/{_locale}/', roles: ROLE_USER}
|
||||
- {path: '^/api', roles: IS_AUTHENTICATED}
|
||||
|
||||
when@test:
|
||||
# this configuration simplifies testing URLs protected by the security mechanism
|
||||
# See https://symfony.com/doc/current/cookbook/testing/http_authentication.html
|
||||
security:
|
||||
password_hashers:
|
||||
# Password hashers are resource-intensive by design to ensure security.
|
||||
# In tests, it's safe to reduce their cost to improve performance.
|
||||
App\Entity\User:
|
||||
algorithm: auto
|
||||
# see https://github.com/symfony/recipes/pull/1026
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
|
||||
@@ -6,20 +6,18 @@
|
||||
#
|
||||
tabler:
|
||||
options:
|
||||
# dark or normal layout
|
||||
theme_auto: true
|
||||
dark_mode: false
|
||||
# condensed = one nav-bar, false = two nav-bars
|
||||
navbar_condensed: true
|
||||
# overlap = https://preview.tabler.io/layout-navbar-overlap.html
|
||||
navbar_overlap: true
|
||||
# boxed layout (true) or full-screen (false)
|
||||
navbar_overlap: false
|
||||
navbar_dark: true
|
||||
boxed_layout: false
|
||||
# right to left = true
|
||||
rtl_mode: false
|
||||
# true = only avatar, false = show username and title as well
|
||||
user_menu_condensed: true
|
||||
# url to your company logo
|
||||
user_menu_condensed: false
|
||||
logo_url: "touch-icon-192x192.png"
|
||||
theme_base: "neutral"
|
||||
theme_radius: 0.5
|
||||
theme_primary: "blue"
|
||||
|
||||
knp_menu:
|
||||
enable: false
|
||||
@@ -29,7 +27,6 @@ tabler:
|
||||
routes:
|
||||
tabler_welcome: dashboard
|
||||
tabler_login: login
|
||||
tabler_logout: logout
|
||||
tabler_login_check: security_check
|
||||
tabler_registration: registration_register
|
||||
tabler_registration_register: registration_register
|
||||
@@ -48,6 +45,7 @@ tabler:
|
||||
barcode: fas fa-barcode
|
||||
bookmark: far fa-star
|
||||
bookmarked: fas fa-star
|
||||
break: fas fa-utensils
|
||||
calendar: far fa-calendar-alt
|
||||
cancel: fas fa-times
|
||||
clock: far fa-clock
|
||||
@@ -69,10 +67,10 @@ tabler:
|
||||
documentation: fas fa-book
|
||||
dot: fas fa-circle
|
||||
download: fas fa-download
|
||||
duration: far fa-hourglass
|
||||
duration: fas fa-stopwatch
|
||||
edit: far fa-edit
|
||||
end: fas fa-stopwatch
|
||||
export: fas fa-file-export
|
||||
end: fas fa-hourglass-end
|
||||
export: fas fa-download
|
||||
failure: fas fa-times
|
||||
fax: fas fa-fax
|
||||
filter: fas fa-filter
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
framework:
|
||||
# https://symfony.com/doc/current/reference/configuration/framework.html#enabled-locales
|
||||
enabled_locales: '%kimai_locales%'
|
||||
default_locale: '%locale%'
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
twig:
|
||||
debug: '%kernel.debug%'
|
||||
strict_variables: '%kernel.debug%'
|
||||
file_name_pattern: '*.twig'
|
||||
form_themes:
|
||||
- 'form/vertical.html.twig'
|
||||
exception_controller: null
|
||||
paths:
|
||||
# used in templates/emails/layout.html.twig
|
||||
'%kernel.project_dir%/assets/css': css
|
||||
'%kernel.project_dir%/templates/bundles/TablerBundle': theme
|
||||
'%kernel.project_dir%/vendor/kevinpapst/tabler-bundle/templates': theme
|
||||
globals:
|
||||
app: '@App\Twig\AppVariable'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { only_exceptions: false }
|
||||
profiler:
|
||||
collect_serializer_data: true
|
||||
|
||||
when@test:
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
||||
profiler:
|
||||
collect: false
|
||||
collect_serializer_data: true
|
||||
|
||||
@@ -1,17 +1,45 @@
|
||||
webpack_encore:
|
||||
# The path where Encore is building the assets.
|
||||
# This should match Encore.setOutputPath() in webpack.config.js.
|
||||
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
|
||||
output_path: '%kernel.project_dir%/public/build'
|
||||
# If multiple builds are defined (as shown below), you can disable the default build:
|
||||
# output_path: false
|
||||
|
||||
# if using Encore.enableIntegrityHashes() specify the crossorigin attribute value (default: false, or use 'anonymous' or 'use-credentials')
|
||||
# crossorigin: 'anonymous'
|
||||
|
||||
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# Available in version 1.2
|
||||
cache: '%kernel.debug%'
|
||||
# Set attributes that will be rendered on all script and link tags
|
||||
script_attributes:
|
||||
defer: true
|
||||
# Uncomment (also under link_attributes) if using Turbo Drive
|
||||
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
|
||||
# 'data-turbo-track': reload
|
||||
# link_attributes:
|
||||
# Uncomment if using Turbo Drive
|
||||
# 'data-turbo-track': reload
|
||||
|
||||
when@dev:
|
||||
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
|
||||
# crossorigin: 'anonymous'
|
||||
|
||||
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
|
||||
# preload: true
|
||||
|
||||
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
|
||||
# strict_mode: false
|
||||
|
||||
# If you have multiple builds:
|
||||
# builds:
|
||||
# frontend: '%kernel.project_dir%/public/frontend/build'
|
||||
|
||||
# pass the build name as the 3rd argument to the Twig functions
|
||||
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||
|
||||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
||||
|
||||
when@prod:
|
||||
webpack_encore:
|
||||
cache: false
|
||||
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# Available in version 1.2
|
||||
cache: true
|
||||
|
||||
#when@test:
|
||||
# webpack_encore:
|
||||
# strict_mode: false
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
|
||||
|
||||
# This file is the entry point to configure the routes of your app.
|
||||
# Methods with the #[Route] attribute are automatically imported.
|
||||
# See also https://symfony.com/doc/current/routing.html
|
||||
|
||||
# To list all registered routes, run the following command:
|
||||
# bin/console debug:router
|
||||
|
||||
controllers:
|
||||
# resource: routing.controllers
|
||||
resource: ../src/Controller/
|
||||
type: attribute
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
api.swagger_ui:
|
||||
path: /api/doc
|
||||
methods: GET
|
||||
defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
|
||||
|
||||
api.swagger:
|
||||
path: /api/doc.json
|
||||
methods: GET
|
||||
defaults: { _controller: nelmio_api_doc.controller.swagger }
|
||||
defaults:
|
||||
_controller: nelmio_api_doc.controller.stoplight
|
||||
|
||||
api:
|
||||
resource: ../src/API/
|
||||
@@ -31,8 +35,6 @@ security:
|
||||
resource: ../src/Controller/Security/
|
||||
type: attribute
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -49,8 +51,6 @@ home:
|
||||
|
||||
homeLocale:
|
||||
path: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /{_locale}/_error
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
@@ -1,7 +0,0 @@
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
||||
6
config/routes/framework.yaml
Normal file
6
config/routes/framework.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||
prefix: /{_locale}/_error
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
3
config/routes/security.yaml
Normal file
3
config/routes/security.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
||||
@@ -1,7 +1,5 @@
|
||||
app.test.active_entries:
|
||||
path: /{_locale}/layou/active_entries
|
||||
controller: App\Controller\LayoutController::activeEntries
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user