mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 17:39:06 +02:00
Compare commits
No commits in common. "master" and "v1.3.3" have entirely different histories.
1027 changed files with 27588 additions and 185073 deletions
|
@ -1,55 +0,0 @@
|
||||||
{
|
|
||||||
{$CADDY_GLOBAL_OPTIONS}
|
|
||||||
|
|
||||||
frankenphp {
|
|
||||||
{$FRANKENPHP_CONFIG}
|
|
||||||
}
|
|
||||||
|
|
||||||
# https://caddyserver.com/docs/caddyfile/directives#sorting-algorithm
|
|
||||||
order mercure after encode
|
|
||||||
order vulcain after reverse_proxy
|
|
||||||
order php_server before file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
{$CADDY_EXTRA_CONFIG}
|
|
||||||
|
|
||||||
{$SERVER_NAME:localhost} {
|
|
||||||
log {
|
|
||||||
# Redact the authorization query parameter that can be set by Mercure
|
|
||||||
format filter {
|
|
||||||
wrap console
|
|
||||||
fields {
|
|
||||||
uri query {
|
|
||||||
replace authorization REDACTED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
root * /app/public
|
|
||||||
encode zstd br gzip
|
|
||||||
|
|
||||||
mercure {
|
|
||||||
# Transport to use (default to Bolt)
|
|
||||||
transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
|
|
||||||
# Publisher JWT key
|
|
||||||
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
|
|
||||||
# Subscriber JWT key
|
|
||||||
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
|
|
||||||
# Allow anonymous subscribers (double-check that it's what you want)
|
|
||||||
anonymous
|
|
||||||
# Enable the subscription API (double-check that it's what you want)
|
|
||||||
subscriptions
|
|
||||||
# Extra directives
|
|
||||||
{$MERCURE_EXTRA_DIRECTIVES}
|
|
||||||
}
|
|
||||||
|
|
||||||
vulcain
|
|
||||||
|
|
||||||
{$CADDY_SERVER_EXTRA_DIRECTIVES}
|
|
||||||
|
|
||||||
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
|
|
||||||
header ?Permissions-Policy "browsing-topics=()"
|
|
||||||
|
|
||||||
php_server
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host
|
|
||||||
; See https://github.com/docker/for-linux/issues/264
|
|
||||||
; The `client_host` below may optionally be replaced with `discover_client_host=yes`
|
|
||||||
; Add `start_with_request=yes` to start debug session on each request
|
|
||||||
xdebug.client_host = host.docker.internal
|
|
|
@ -1,18 +0,0 @@
|
||||||
expose_php = 0
|
|
||||||
date.timezone = UTC
|
|
||||||
apc.enable_cli = 1
|
|
||||||
session.use_strict_mode = 1
|
|
||||||
zend.detect_unicode = 0
|
|
||||||
|
|
||||||
; https://symfony.com/doc/current/performance.html
|
|
||||||
realpath_cache_size = 4096K
|
|
||||||
realpath_cache_ttl = 600
|
|
||||||
opcache.interned_strings_buffer = 16
|
|
||||||
opcache.max_accelerated_files = 20000
|
|
||||||
opcache.memory_consumption = 256
|
|
||||||
opcache.enable_file_override = 1
|
|
||||||
|
|
||||||
memory_limit = 256M
|
|
||||||
|
|
||||||
upload_max_filesize=256M
|
|
||||||
post_max_size=300M
|
|
|
@ -1,2 +0,0 @@
|
||||||
opcache.preload_user = root
|
|
||||||
opcache.preload = /app/config/preload.php
|
|
|
@ -1,60 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
|
|
||||||
# Install the project the first time PHP is started
|
|
||||||
# After the installation, the following block can be deleted
|
|
||||||
if [ ! -f composer.json ]; then
|
|
||||||
rm -Rf tmp/
|
|
||||||
composer create-project "symfony/skeleton $SYMFONY_VERSION" tmp --stability="$STABILITY" --prefer-dist --no-progress --no-interaction --no-install
|
|
||||||
|
|
||||||
cd tmp
|
|
||||||
cp -Rp . ..
|
|
||||||
cd -
|
|
||||||
rm -Rf tmp/
|
|
||||||
|
|
||||||
composer require "php:>=$PHP_VERSION" runtime/frankenphp-symfony
|
|
||||||
composer config --json extra.symfony.docker 'true'
|
|
||||||
|
|
||||||
if grep -q ^DATABASE_URL= .env; then
|
|
||||||
echo "To finish the installation please press Ctrl+C to stop Docker Compose and run: docker compose up --build -d --wait"
|
|
||||||
sleep infinity
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then
|
|
||||||
composer install --prefer-dist --no-progress --no-interaction
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -q ^DATABASE_URL= .env; then
|
|
||||||
echo "Waiting for database to be ready..."
|
|
||||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
|
||||||
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
|
|
||||||
if [ $? -eq 255 ]; then
|
|
||||||
# If the Doctrine command exits with 255, an unrecoverable error occurred
|
|
||||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
|
|
||||||
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
|
|
||||||
echo "The database is not up or not reachable:"
|
|
||||||
echo "$DATABASE_ERROR"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "The database is now ready and reachable"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then
|
|
||||||
php bin/console doctrine:migrations:migrate --no-interaction
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
|
||||||
setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec docker-php-entrypoint "$@"
|
|
|
@ -1,4 +0,0 @@
|
||||||
worker {
|
|
||||||
file ./public/index.php
|
|
||||||
env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
|
|
||||||
}
|
|
|
@ -39,50 +39,8 @@ if [ -d /var/www/html/var/db ]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Start PHP-FPM (the PHP_VERSION is replaced by the configured version in the Dockerfile)
|
# Start PHP-FPM
|
||||||
service phpPHP_VERSION-fpm start
|
service php8.1-fpm start
|
||||||
|
|
||||||
|
|
||||||
# Run migrations if automigration is enabled via env variable DB_AUTOMIGRATE
|
|
||||||
if [ "$DB_AUTOMIGRATE" = "true" ]; then
|
|
||||||
echo "Waiting for database to be ready..."
|
|
||||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
|
||||||
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(sudo -E -u www-data php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
|
|
||||||
if [ $? -eq 255 ]; then
|
|
||||||
# If the Doctrine command exits with 255, an unrecoverable error occurred
|
|
||||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
|
|
||||||
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
|
|
||||||
echo "The database is not up or not reachable:"
|
|
||||||
echo "$DATABASE_ERROR"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "The database is now ready and reachable"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if there are any available migrations to do, by executing doctrine:migrations:up-to-date
|
|
||||||
# and checking if the exit code is 0 (up to date) or 1 (not up to date)
|
|
||||||
if sudo -E -u www-data php bin/console doctrine:migrations:up-to-date --no-interaction; then
|
|
||||||
echo "Database is up to date, no migrations necessary."
|
|
||||||
else
|
|
||||||
echo "Migrations available..."
|
|
||||||
echo "Do backup of database..."
|
|
||||||
|
|
||||||
sudo -E -u www-data mkdir -p /var/www/html/uploads/.automigration-backup/
|
|
||||||
# Backup the database
|
|
||||||
sudo -E -u www-data php bin/console partdb:backup -n --database /var/www/html/uploads/.automigration-backup/backup-$(date +%Y-%m-%d_%H-%M-%S).zip
|
|
||||||
|
|
||||||
# Check if there are any migration files
|
|
||||||
sudo -E -u www-data php bin/console doctrine:migrations:migrate --no-interaction
|
|
||||||
fi
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
# first arg is `-f` or `--some-option` (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/docker-php-entrypoint)
|
# first arg is `-f` or `--some-option` (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/docker-php-entrypoint)
|
||||||
if [ "${1#-}" != "$1" ]; then
|
if [ "${1#-}" != "$1" ]; then
|
||||||
|
|
|
@ -25,28 +25,15 @@
|
||||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||||
|
|
||||||
# Pass the configuration from the docker env to the PHP environment (here you should list all .env options)
|
# Pass the configuration from the docker env to the PHP environment (here you should list all .env options)
|
||||||
PassEnv APP_ENV APP_DEBUG APP_SECRET REDIRECT_TO_HTTPS DISABLE_YEAR2038_BUG_CHECK
|
PassEnv APP_ENV APP_DEBUG APP_SECRET
|
||||||
PassEnv TRUSTED_PROXIES TRUSTED_HOSTS LOCK_DSN
|
PassEnv DATABASE_URL ENFORCE_CHANGE_COMMENTS_FOR
|
||||||
PassEnv DATABASE_URL ENFORCE_CHANGE_COMMENTS_FOR DATABASE_MYSQL_USE_SSL_CA DATABASE_MYSQL_SSL_VERIFY_CERT
|
PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI
|
||||||
PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI CHECK_FOR_UPDATES ATTACHMENT_DOWNLOAD_BY_DEFAULT
|
|
||||||
PassEnv MAILER_DSN ALLOW_EMAIL_PW_RESET EMAIL_SENDER_EMAIL EMAIL_SENDER_NAME
|
PassEnv MAILER_DSN ALLOW_EMAIL_PW_RESET EMAIL_SENDER_EMAIL EMAIL_SENDER_NAME
|
||||||
PassEnv HISTORY_SAVE_CHANGED_FIELDS HISTORY_SAVE_CHANGED_DATA HISTORY_SAVE_REMOVED_DATA HISTORY_SAVE_NEW_DATA
|
PassEnv HISTORY_SAVE_CHANGED_FIELDS HISTORY_SAVE_CHANGED_DATA HISTORY_SAVE_REMOVED_DATA
|
||||||
PassEnv ERROR_PAGE_ADMIN_EMAIL ERROR_PAGE_SHOW_HELP
|
PassEnv ERROR_PAGE_ADMIN_EMAIL ERROR_PAGE_SHOW_HELP
|
||||||
PassEnv DEMO_MODE NO_URL_REWRITE_AVAILABLE FIXER_API_KEY BANNER
|
PassEnv DEMO_MODE NO_URL_REWRITE_AVAILABLE FIXER_API_KEY BANNER
|
||||||
# In old version the SAML sp private key env, was wrongly named SAMLP_SP_PRIVATE_KEY, keep it for backward compatibility
|
PassEnv SAML_ENABLED SAML_ROLE_MAPPING SAML_UPDATE_GROUP_ON_LOGIN SAML_IDP_ENTITY_ID SAML_IDP_SINGLE_SIGN_ON_SERVICE SAML_IDP_SINGLE_LOGOUT_SERVICE SAML_IDP_X509_CERT SAML_SP_ENTITY_ID SAML_SP_X509_CERT SAMLP_SP_PRIVATE_KEY
|
||||||
PassEnv SAML_ENABLED SAML_BEHIND_PROXY SAML_ROLE_MAPPING SAML_UPDATE_GROUP_ON_LOGIN SAML_IDP_ENTITY_ID SAML_IDP_SINGLE_SIGN_ON_SERVICE SAML_IDP_SINGLE_LOGOUT_SERVICE SAML_IDP_X509_CERT SAML_SP_ENTITY_ID SAML_SP_X509_CERT SAML_SP_PRIVATE_KEY SAMLP_SP_PRIVATE_KEY
|
|
||||||
PassEnv TABLE_DEFAULT_PAGE_SIZE TABLE_PARTS_DEFAULT_COLUMNS
|
|
||||||
|
|
||||||
PassEnv PROVIDER_DIGIKEY_CLIENT_ID PROVIDER_DIGIKEY_SECRET PROVIDER_DIGIKEY_CURRENCY PROVIDER_DIGIKEY_LANGUAGE PROVIDER_DIGIKEY_COUNTRY
|
|
||||||
PassEnv PROVIDER_ELEMENT14_KEY PROVIDER_ELEMENT14_STORE_ID
|
|
||||||
PassEnv PROVIDER_TME_KEY PROVIDER_TME_SECRET PROVIDER_TME_CURRENCY PROVIDER_TME_LANGUAGE PROVIDER_TME_COUNTRY PROVIDER_TME_GET_GROSS_PRICES
|
|
||||||
PassEnv PROVIDER_OCTOPART_CLIENT_ID PROVIDER_OCTOPART_SECRET PROVIDER_OCTOPART_CURRENCY PROVIDER_OCTOPART_COUNTRY PROVIDER_OCTOPART_SEARCH_LIMIT PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS
|
|
||||||
PassEnv PROVIDER_MOUSER_KEY PROVIDER_MOUSER_SEARCH_OPTION PROVIDER_MOUSER_SEARCH_LIMIT PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE
|
|
||||||
PassEnv PROVIDER_LCSC_ENABLED PROVIDER_LCSC_CURRENCY
|
|
||||||
PassEnv PROVIDER_OEMSECRETS_KEY PROVIDER_OEMSECRETS_COUNTRY_CODE PROVIDER_OEMSECRETS_CURRENCY PROVIDER_OEMSECRETS_ZERO_PRICE PROVIDER_OEMSECRETS_SET_PARAM PROVIDER_OEMSECRETS_SORT_CRITERIA
|
|
||||||
PassEnv PROVIDER_REICHELT_ENABLED PROVIDER_REICHELT_CURRENCY PROVIDER_REICHELT_COUNTRY PROVIDER_REICHELT_LANGUAGE PROVIDER_REICHELT_INCLUDE_VAT
|
|
||||||
PassEnv PROVIDER_POLLIN_ENABLED
|
|
||||||
PassEnv EDA_KICAD_CATEGORY_DEPTH
|
|
||||||
|
|
||||||
# For most configuration files from conf-available/, which are
|
# For most configuration files from conf-available/, which are
|
||||||
# enabled or disabled at a global level, it is possible to
|
# enabled or disabled at a global level, it is possible to
|
||||||
|
|
|
@ -5,8 +5,6 @@ tests/
|
||||||
docs/
|
docs/
|
||||||
.git
|
.git
|
||||||
|
|
||||||
/public/media/*
|
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
/.env.local
|
/.env.local
|
||||||
/.env.local.php
|
/.env.local.php
|
||||||
|
@ -44,39 +42,3 @@ yarn-error.log
|
||||||
/phpunit.xml
|
/phpunit.xml
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
###< phpunit/phpunit ###
|
###< phpunit/phpunit ###
|
||||||
|
|
||||||
|
|
||||||
### From frankenphp
|
|
||||||
|
|
||||||
**/*.log
|
|
||||||
**/*.php~
|
|
||||||
**/*.dist.php
|
|
||||||
**/*.dist
|
|
||||||
**/*.cache
|
|
||||||
**/._*
|
|
||||||
**/.dockerignore
|
|
||||||
**/.DS_Store
|
|
||||||
**/.git/
|
|
||||||
**/.gitattributes
|
|
||||||
**/.gitignore
|
|
||||||
**/.gitmodules
|
|
||||||
**/compose.*.yaml
|
|
||||||
**/compose.*.yml
|
|
||||||
**/compose.yaml
|
|
||||||
**/compose.yml
|
|
||||||
**/docker-compose.*.yaml
|
|
||||||
**/docker-compose.*.yml
|
|
||||||
**/docker-compose.yaml
|
|
||||||
**/docker-compose.yml
|
|
||||||
**/Dockerfile
|
|
||||||
**/Thumbs.db
|
|
||||||
.github/
|
|
||||||
public/bundles/
|
|
||||||
var/
|
|
||||||
vendor/
|
|
||||||
.editorconfig
|
|
||||||
.env.*.local
|
|
||||||
.env.local
|
|
||||||
.env.local.php
|
|
||||||
.env.test
|
|
||||||
|
|
||||||
|
|
184
.env
184
.env
|
@ -14,19 +14,6 @@ DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"
|
||||||
# Uncomment this line (and comment the line above to use a MySQL database
|
# Uncomment this line (and comment the line above to use a MySQL database
|
||||||
#DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db?serverVersion=5.7
|
#DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db?serverVersion=5.7
|
||||||
|
|
||||||
# Set this value to 1, if you want to use SSL to connect to the MySQL server. It will be tried to use the CA certificate
|
|
||||||
# otherwise a CA bundle shipped with PHP will be used.
|
|
||||||
# Leave it at 0, if you do not want to use SSL or if your server does not support it
|
|
||||||
DATABASE_MYSQL_USE_SSL_CA=0
|
|
||||||
|
|
||||||
# Set this value to 0, if you don't want to verify the CA certificate of the MySQL server
|
|
||||||
# Only do this, if you know what you are doing!
|
|
||||||
DATABASE_MYSQL_SSL_VERIFY_CERT=1
|
|
||||||
|
|
||||||
# Emulate natural sorting of strings even on databases that do not support it (like SQLite, MySQL or MariaDB < 10.7)
|
|
||||||
# This can be slow on big databases and might have some problems and quirks, so use it with caution
|
|
||||||
DATABASE_EMULATE_NATURAL_SORT=0
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# General settings
|
# General settings
|
||||||
###################################################################################
|
###################################################################################
|
||||||
|
@ -42,15 +29,13 @@ INSTANCE_NAME="Part-DB"
|
||||||
# Allow users to download attachments to the server by providing an URL
|
# Allow users to download attachments to the server by providing an URL
|
||||||
# This could be a potential security issue, as the user can retrieve any file the server has access to (via internet)
|
# This could be a potential security issue, as the user can retrieve any file the server has access to (via internet)
|
||||||
ALLOW_ATTACHMENT_DOWNLOADS=0
|
ALLOW_ATTACHMENT_DOWNLOADS=0
|
||||||
# Set this to 1, if the "download external files" checkbox should be checked by default for new attachments
|
|
||||||
ATTACHMENT_DOWNLOAD_BY_DEFAULT=0
|
|
||||||
# Use gravatars for user avatars, when user has no own avatar defined
|
# Use gravatars for user avatars, when user has no own avatar defined
|
||||||
USE_GRAVATAR=0
|
USE_GRAVATAR=0
|
||||||
# The maximum allowed size for attachment files in bytes (you can use M for megabytes and G for gigabytes)
|
# The maximum allowed size for attachment files in bytes (you can use M for megabytes and G for gigabytes)
|
||||||
# Please note that the php.ini setting upload_max_filesize also limits the maximum size of uploaded files
|
# Please note that the php.ini setting upload_max_filesize also limits the maximum size of uploaded files
|
||||||
MAX_ATTACHMENT_FILE_SIZE="100M"
|
MAX_ATTACHMENT_FILE_SIZE="100M"
|
||||||
|
|
||||||
# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates
|
# The public reachable URL of this Part-DB installation. This is used for generating links to the website in emails and so on
|
||||||
# This must end with a slash!
|
# This must end with a slash!
|
||||||
DEFAULT_URI="https://partdb.changeme.invalid/"
|
DEFAULT_URI="https://partdb.changeme.invalid/"
|
||||||
|
|
||||||
|
@ -59,9 +44,6 @@ DEFAULT_URI="https://partdb.changeme.invalid/"
|
||||||
# Leave this empty, to make all change reasons optional
|
# Leave this empty, to make all change reasons optional
|
||||||
ENFORCE_CHANGE_COMMENTS_FOR=""
|
ENFORCE_CHANGE_COMMENTS_FOR=""
|
||||||
|
|
||||||
# Disable that if you do not want that Part-DB connects to GitHub to check for available updates, or if your server can not connect to the internet
|
|
||||||
CHECK_FOR_UPDATES=1
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# Email settings
|
# Email settings
|
||||||
###################################################################################
|
###################################################################################
|
||||||
|
@ -89,9 +71,6 @@ HISTORY_SAVE_CHANGED_FIELDS=1
|
||||||
HISTORY_SAVE_CHANGED_DATA=1
|
HISTORY_SAVE_CHANGED_DATA=1
|
||||||
# Save the data of an element that gets removed into log entry. This allows to undelete an element
|
# Save the data of an element that gets removed into log entry. This allows to undelete an element
|
||||||
HISTORY_SAVE_REMOVED_DATA=1
|
HISTORY_SAVE_REMOVED_DATA=1
|
||||||
# Save the new data of an element that gets changed or added. This allows an easy comparison of the old and new data on the detail page
|
|
||||||
# This option only becomes active when HISTORY_SAVE_CHANGED_DATA is set to 1
|
|
||||||
HISTORY_SAVE_NEW_DATA=1
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# Error pages settings
|
# Error pages settings
|
||||||
|
@ -102,160 +81,12 @@ ERROR_PAGE_ADMIN_EMAIL=''
|
||||||
# If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them...
|
# If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them...
|
||||||
ERROR_PAGE_SHOW_HELP=1
|
ERROR_PAGE_SHOW_HELP=1
|
||||||
|
|
||||||
##################################################################################
|
|
||||||
# Part table settings
|
|
||||||
##################################################################################
|
|
||||||
|
|
||||||
# The default page size for the part table (set to -1 to show all parts on one page)
|
|
||||||
TABLE_DEFAULT_PAGE_SIZE=50
|
|
||||||
# Configure which columns will be visible by default in the parts table (and in which order).
|
|
||||||
# This is a comma separated list of column names. See documentation for available values.
|
|
||||||
TABLE_PARTS_DEFAULT_COLUMNS=name,description,category,footprint,manufacturer,storage_location,amount
|
|
||||||
|
|
||||||
##################################################################################
|
|
||||||
# Info provider settings
|
|
||||||
##################################################################################
|
|
||||||
|
|
||||||
# Digikey Provider:
|
|
||||||
# You can get your client id and secret from https://developer.digikey.com/
|
|
||||||
PROVIDER_DIGIKEY_CLIENT_ID=
|
|
||||||
PROVIDER_DIGIKEY_SECRET=
|
|
||||||
# The currency to get prices in
|
|
||||||
PROVIDER_DIGIKEY_CURRENCY=EUR
|
|
||||||
# The language to get results in (en, de, fr, it, es, zh, ja, ko)
|
|
||||||
PROVIDER_DIGIKEY_LANGUAGE=en
|
|
||||||
# The country to get results for
|
|
||||||
PROVIDER_DIGIKEY_COUNTRY=DE
|
|
||||||
|
|
||||||
# Farnell Provider:
|
|
||||||
# You can get your API key from https://partner.element14.com/
|
|
||||||
PROVIDER_ELEMENT14_KEY=
|
|
||||||
# Configure the store domain you want to use. This decides the language and currency of results. You can get a list of available stores from https://partner.element14.com/docs/Product_Search_API_REST__Description
|
|
||||||
PROVIDER_ELEMENT14_STORE_ID=de.farnell.com
|
|
||||||
|
|
||||||
# TME Provider:
|
|
||||||
# You can get your API key from https://developers.tme.eu/en/
|
|
||||||
PROVIDER_TME_KEY=
|
|
||||||
PROVIDER_TME_SECRET=
|
|
||||||
# The currency to get prices in
|
|
||||||
PROVIDER_TME_CURRENCY=EUR
|
|
||||||
# The language to get results in (en, de, pl)
|
|
||||||
PROVIDER_TME_LANGUAGE=en
|
|
||||||
# The country to get results for
|
|
||||||
PROVIDER_TME_COUNTRY=DE
|
|
||||||
# [DEPRECATED] Set this to 1 to get gross prices (including VAT) instead of net prices
|
|
||||||
# With private API keys, this option cannot be used anymore is ignored by Part-DB. The VAT inclusion depends on your TME account settings.
|
|
||||||
PROVIDER_TME_GET_GROSS_PRICES=1
|
|
||||||
|
|
||||||
# Octopart / Nexar Provider:
|
|
||||||
# You can get your API key from https://nexar.com/api
|
|
||||||
PROVIDER_OCTOPART_CLIENT_ID=
|
|
||||||
PROVIDER_OCTOPART_SECRET=
|
|
||||||
# The currency and country to get prices for (you have to set both to get meaningful results)
|
|
||||||
# 3 letter ISO currency code (e.g. EUR, USD, GBP)
|
|
||||||
PROVIDER_OCTOPART_CURRENCY=EUR
|
|
||||||
# 2 letter ISO country code (e.g. DE, US, GB)
|
|
||||||
PROVIDER_OCTOPART_COUNTRY=DE
|
|
||||||
# The number of results to get from Octopart while searching (please note that this counts towards your API limits)
|
|
||||||
PROVIDER_OCTOPART_SEARCH_LIMIT=10
|
|
||||||
# Set to false to include non authorized offers in the results
|
|
||||||
PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS=1
|
|
||||||
|
|
||||||
# Mouser Provider API V2:
|
|
||||||
# You can get your API key from https://www.mouser.it/api-hub/
|
|
||||||
PROVIDER_MOUSER_KEY=
|
|
||||||
# Filter search results by RoHS compliance and stock availability:
|
|
||||||
# Available options: None | Rohs | InStock | RohsAndInStock
|
|
||||||
PROVIDER_MOUSER_SEARCH_OPTION='None'
|
|
||||||
# The number of results to get from Mouser while searching (please note that this value is max 50)
|
|
||||||
PROVIDER_MOUSER_SEARCH_LIMIT=50
|
|
||||||
# It is recommended to leave this set to 'true'. The option is not really good doumented by Mouser:
|
|
||||||
# Used when searching for keywords in the language specified when you signed up for Search API.
|
|
||||||
PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE='true'
|
|
||||||
|
|
||||||
# LCSC Provider:
|
|
||||||
# LCSC does not provide an offical API, so this used the API LCSC uses to render their webshop.
|
|
||||||
# LCSC did not intended the use of this API and it could break any time, so use it at your own risk.
|
|
||||||
|
|
||||||
# We dont require an API key for LCSC, just set this to 1 to enable LCSC support
|
|
||||||
PROVIDER_LCSC_ENABLED=0
|
|
||||||
# The currency to get prices in (e.g. EUR, USD, etc.)
|
|
||||||
PROVIDER_LCSC_CURRENCY=EUR
|
|
||||||
|
|
||||||
# Oemsecrets Provider API 3.0.1:
|
|
||||||
# You can get your API key from https://www.oemsecrets.com/api
|
|
||||||
PROVIDER_OEMSECRETS_KEY=
|
|
||||||
# The country you want the output for
|
|
||||||
PROVIDER_OEMSECRETS_COUNTRY_CODE=DE
|
|
||||||
# Available country code are:
|
|
||||||
# AD, AE, AQ, AR, AT, AU, BE, BO, BR, BV, BY, CA, CH, CL, CN, CO, CZ, DE, DK, EC, EE, EH,
|
|
||||||
# ES, FI, FK, FO, FR, GB, GE, GF, GG, GI, GL, GR, GS, GY, HK, HM, HR, HU, IE, IM, IN, IS,
|
|
||||||
# IT, JM, JP, KP, KR, KZ, LI, LK, LT, LU, MC, MD, ME, MK, MT, NL, NO, NZ, PE, PH, PL, PT,
|
|
||||||
# PY, RO, RS, RU, SB, SD, SE, SG, SI, SJ, SK, SM, SO, SR, SY, SZ, TC, TF, TG, TH, TJ, TK,
|
|
||||||
# TM, TN, TO, TR, TT, TV, TW, TZ, UA, UG, UM, US, UY, UZ, VA, VE, VG, VI, VN, VU, WF, YE,
|
|
||||||
# ZA, ZM, ZW
|
|
||||||
#
|
|
||||||
# The currency you want the prices to be displayed in
|
|
||||||
PROVIDER_OEMSECRETS_CURRENCY=EUR
|
|
||||||
# Available currency are:AUD, CAD, CHF, CNY, DKK, EUR, GBP, HKD, ILS, INR, JPY, KRW, NOK,
|
|
||||||
# NZD, RUB, SEK, SGD, TWD, USD
|
|
||||||
#
|
|
||||||
# If PROVIDER_OEMSECRETS_ZERO_PRICE is set to 0, distributors with zero prices
|
|
||||||
# will be discarded from the creation of a new part (set to 1 otherwise)
|
|
||||||
PROVIDER_OEMSECRETS_ZERO_PRICE=0
|
|
||||||
#
|
|
||||||
# When PROVIDER_OEMSECRETS_SET_PARAM is set to 1 the parameters for the part are generated
|
|
||||||
# from the description transforming unstructured descriptions into structured parameters;
|
|
||||||
# each parameter in description should have the form: "...;name1:value1;name2:value2"
|
|
||||||
PROVIDER_OEMSECRETS_SET_PARAM=1
|
|
||||||
#
|
|
||||||
# This environment variable determines the sorting criteria for product results.
|
|
||||||
# The sorting process first arranges items based on the provided keyword.
|
|
||||||
# Then, if set to 'C', it further sorts by completeness (prioritizing items with the most
|
|
||||||
# detailed information). If set to 'M', it further sorts by manufacturer name.
|
|
||||||
#If unset or set to any other value, no sorting is performed.
|
|
||||||
PROVIDER_OEMSECRETS_SORT_CRITERIA=C
|
|
||||||
|
|
||||||
|
|
||||||
# Reichelt provider:
|
|
||||||
# Reichelt.com offers no official API, so this info provider webscrapes the website to extract info
|
|
||||||
# It could break at any time, use it at your own risk
|
|
||||||
# We dont require an API key for Reichelt, just set this to 1 to enable Reichelt support
|
|
||||||
PROVIDER_REICHELT_ENABLED=0
|
|
||||||
# The country to get prices for
|
|
||||||
PROVIDER_REICHELT_COUNTRY=DE
|
|
||||||
# The language to get results in (en, de, fr, nl, pl, it, es)
|
|
||||||
PROVIDER_REICHELT_LANGUAGE=en
|
|
||||||
# Include VAT in prices (set to 1 to include VAT, 0 to exclude VAT)
|
|
||||||
PROVIDER_REICHELT_INCLUDE_VAT=1
|
|
||||||
# The currency to get prices in (only for countries with countries other than EUR)
|
|
||||||
PROVIDER_REICHELT_CURRENCY=EUR
|
|
||||||
|
|
||||||
# Pollin provider:
|
|
||||||
# Pollin.de offers no official API, so this info provider webscrapes the website to extract info
|
|
||||||
# It could break at any time, use it at your own risk
|
|
||||||
# We dont require an API key for Pollin, just set this to 1 to enable Pollin support
|
|
||||||
PROVIDER_POLLIN_ENABLED=0
|
|
||||||
|
|
||||||
##################################################################################
|
|
||||||
# EDA integration related settings
|
|
||||||
##################################################################################
|
|
||||||
|
|
||||||
# This value determines the depth of the category tree, that is visible inside KiCad
|
|
||||||
# 0 means that only the top level categories are visible. Set to a value > 0 to show more levels.
|
|
||||||
# Set to -1, to show all parts of Part-DB inside a single category in KiCad
|
|
||||||
EDA_KICAD_CATEGORY_DEPTH=0
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# SAML Single sign on-settings
|
# SAML Single sign on-settings
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# Set this to 1 to enable SAML single sign on
|
# Set this to 1 to enable SAML single sign on
|
||||||
# Be also sure to set the correct values for DEFAULT_URI
|
|
||||||
SAML_ENABLED=0
|
SAML_ENABLED=0
|
||||||
|
|
||||||
# Set to 1, if your Part-DB installation is behind a reverse proxy and you want to use SAML
|
|
||||||
SAML_BEHIND_PROXY=0
|
|
||||||
|
|
||||||
# A JSON encoded array of role mappings in the form { "saml_role": PARTDB_GROUP_ID, "*": PARTDB_GROUP_ID }
|
# A JSON encoded array of role mappings in the form { "saml_role": PARTDB_GROUP_ID, "*": PARTDB_GROUP_ID }
|
||||||
# The first match is used, so the order is important! Put the group mapping with the most privileges first.
|
# The first match is used, so the order is important! Put the group mapping with the most privileges first.
|
||||||
# Please not to only use single quotes to enclose the JSON string
|
# Please not to only use single quotes to enclose the JSON string
|
||||||
|
@ -282,7 +113,7 @@ SAML_SP_ENTITY_ID="https://partdb.changeme.invalid/sp"
|
||||||
# The public certificate of the SAML SP
|
# The public certificate of the SAML SP
|
||||||
SAML_SP_X509_CERT="MIIC..."
|
SAML_SP_X509_CERT="MIIC..."
|
||||||
# The private key of the SAML SP
|
# The private key of the SAML SP
|
||||||
SAML_SP_PRIVATE_KEY="MIIE..."
|
SAMLP_SP_PRIVATE_KEY="MIIE..."
|
||||||
|
|
||||||
|
|
||||||
######################################################################################
|
######################################################################################
|
||||||
|
@ -295,9 +126,6 @@ DEMO_MODE=0
|
||||||
# In that case all URL contains the index.php front controller in URL
|
# In that case all URL contains the index.php front controller in URL
|
||||||
NO_URL_REWRITE_AVAILABLE=0
|
NO_URL_REWRITE_AVAILABLE=0
|
||||||
|
|
||||||
# Set to 1, if Part-DB should redirect all HTTP requests to HTTPS. You dont need to configure this, if your webserver already does this.
|
|
||||||
REDIRECT_TO_HTTPS=0
|
|
||||||
|
|
||||||
# If you want to use fixer.io for currency conversion, you have to set this to your API key
|
# If you want to use fixer.io for currency conversion, you have to set this to your API key
|
||||||
FIXER_API_KEY=CHANGEME
|
FIXER_API_KEY=CHANGEME
|
||||||
|
|
||||||
|
@ -308,11 +136,9 @@ BANNER=""
|
||||||
APP_ENV=prod
|
APP_ENV=prod
|
||||||
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
|
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
|
||||||
|
|
||||||
# Set this to zero, if you want to disable the year 2038 bug check on 32-bit systems (it will cause errors with current 32-bit PHP versions)
|
|
||||||
DISABLE_YEAR2038_BUG_CHECK=0
|
|
||||||
|
|
||||||
# Set the trusted IPs here, when using an reverse proxy
|
# Set the trusted IPs here, when using an reverse proxy
|
||||||
#TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||||
#TRUSTED_HOSTS='^(localhost|example\.com)$'
|
#TRUSTED_HOSTS='^(localhost|example\.com)$'
|
||||||
|
|
||||||
|
|
||||||
|
@ -321,7 +147,3 @@ DISABLE_YEAR2038_BUG_CHECK=0
|
||||||
# postgresql+advisory://db_user:db_password@localhost/db_name
|
# postgresql+advisory://db_user:db_password@localhost/db_name
|
||||||
LOCK_DSN=flock
|
LOCK_DSN=flock
|
||||||
###< symfony/lock ###
|
###< symfony/lock ###
|
||||||
|
|
||||||
###> nelmio/cors-bundle ###
|
|
||||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
|
||||||
###< nelmio/cors-bundle ###
|
|
||||||
|
|
0
.env.dev
0
.env.dev
|
@ -5,9 +5,5 @@ SYMFONY_DEPRECATIONS_HELPER=999999
|
||||||
PANTHER_APP_ENV=panther
|
PANTHER_APP_ENV=panther
|
||||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||||
|
|
||||||
DATABASE_URL="sqlite:///%kernel.project_dir%/var/app_test.db"
|
|
||||||
# Doctrine automatically adds an _test suffix to database name in test env
|
# Doctrine automatically adds an _test suffix to database name in test env
|
||||||
#DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db
|
DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db
|
||||||
|
|
||||||
# Disable update checks, as tests would fail, when github is not reachable
|
|
||||||
CHECK_FOR_UPDATES=0
|
|
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,2 +0,0 @@
|
||||||
# For sh files, always use LF line endings
|
|
||||||
*.sh text eol=lf
|
|
20
.github/workflows/assets_artifact_build.yml
vendored
20
.github/workflows/assets_artifact_build.yml
vendored
|
@ -19,22 +19,14 @@ jobs:
|
||||||
APP_ENV: prod
|
APP_ENV: prod
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.2'
|
|
||||||
coverage: none
|
|
||||||
ini-values: xdebug.max_nesting_level=1000
|
|
||||||
extensions: mbstring, intl, gd, xsl, gmp, bcmath, :php-psr
|
|
||||||
|
|
||||||
- name: Get Composer Cache Directory
|
- name: Get Composer Cache Directory
|
||||||
id: composer-cache
|
id: composer-cache
|
||||||
run: |
|
run: |
|
||||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
@ -48,7 +40,7 @@ jobs:
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v3
|
||||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
@ -57,7 +49,7 @@ jobs:
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
|
|
||||||
|
@ -77,13 +69,13 @@ jobs:
|
||||||
run: zip -r /tmp/partdb_assets.zip public/build/ vendor/
|
run: zip -r /tmp/partdb_assets.zip public/build/ vendor/
|
||||||
|
|
||||||
- name: Upload assets artifact
|
- name: Upload assets artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Only dependencies and built assets
|
name: Only dependencies and built assets
|
||||||
path: /tmp/partdb_assets.zip
|
path: /tmp/partdb_assets.zip
|
||||||
|
|
||||||
- name: Upload full artifact
|
- name: Upload full artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Full Part-DB including dependencies and built assets
|
name: Full Part-DB including dependencies and built assets
|
||||||
path: /tmp/partdb_with_assets.zip
|
path: /tmp/partdb_with_assets.zip
|
||||||
|
|
54
.github/workflows/codeql-analysis.yml
vendored
Normal file
54
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [master]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 14 * * 3'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyse:
|
||||||
|
name: Analyse
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# We must fetch at least the immediate parents so that if this is
|
||||||
|
# a pull request then we can checkout the head.
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
# If this run was triggered by a pull request event, then checkout
|
||||||
|
# the head of the pull request instead of the merge commit.
|
||||||
|
- run: git checkout HEAD^2
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
# Override language selection by uncommenting this and choosing your languages
|
||||||
|
# with:
|
||||||
|
# languages: go, javascript, csharp, python, cpp, java
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
12
.github/workflows/docker_build.yml
vendored
12
.github/workflows/docker_build.yml
vendored
|
@ -17,11 +17,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
-
|
-
|
||||||
name: Docker meta
|
name: Docker meta
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
# list of Docker images to use as base name for tags
|
# list of Docker images to use as base name for tags
|
||||||
images: |
|
images: |
|
||||||
|
@ -49,23 +49,23 @@ jobs:
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
with:
|
with:
|
||||||
platforms: 'arm64,arm'
|
platforms: 'arm64,arm'
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Build and push
|
name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
|
77
.github/workflows/docker_frankenphp.yml
vendored
77
.github/workflows/docker_frankenphp.yml
vendored
|
@ -1,77 +0,0 @@
|
||||||
name: Docker Image Build (FrankenPHP)
|
|
||||||
|
|
||||||
on:
|
|
||||||
#schedule:
|
|
||||||
# - cron: '0 10 * * *' # everyday at 10am
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '**'
|
|
||||||
- '!l10n_**'
|
|
||||||
tags:
|
|
||||||
- 'v*.*.*'
|
|
||||||
- 'v*.*.*-**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
-
|
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
-
|
|
||||||
name: Docker meta
|
|
||||||
id: docker_meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
# list of Docker images to use as base name for tags
|
|
||||||
images: |
|
|
||||||
partdborg/part-db
|
|
||||||
# Mark the image build from master as latest (as we dont have really releases yet)
|
|
||||||
tags: |
|
|
||||||
type=edge,branch=master
|
|
||||||
type=ref,event=branch,
|
|
||||||
type=ref,event=tag,
|
|
||||||
type=schedule
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=pr
|
|
||||||
labels: |
|
|
||||||
org.opencontainers.image.source=${{ github.event.repository.clone_url }}
|
|
||||||
org.opencontainers.image.revision=${{ github.sha }}
|
|
||||||
org.opencontainers.image.title=Part-DB
|
|
||||||
org.opencontainers.image.description=Part-DB is a web application for managing electronic components and your inventory.
|
|
||||||
org.opencontainers.image.url=https://github.com/Part-DB/Part-DB-server
|
|
||||||
org.opencontainers.image.source=https://github.com/Part-DB/Part-DB-server
|
|
||||||
org.opencontainers.image.authors=Jan Böhmer
|
|
||||||
org.opencontainers.licenses=AGPL-3.0-or-later
|
|
||||||
|
|
||||||
-
|
|
||||||
name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
with:
|
|
||||||
platforms: 'arm64,arm'
|
|
||||||
-
|
|
||||||
name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
-
|
|
||||||
name: Login to DockerHub
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
-
|
|
||||||
name: Build and push
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfile-frankenphp
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
20
.github/workflows/static_analysis.yml
vendored
20
.github/workflows/static_analysis.yml
vendored
|
@ -16,22 +16,14 @@ jobs:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.2'
|
|
||||||
coverage: none
|
|
||||||
ini-values: xdebug.max_nesting_level=1000
|
|
||||||
extensions: mbstring, intl, gd, xsl, gmp, bcmath, :php-psr
|
|
||||||
|
|
||||||
- name: Get Composer Cache Directory
|
- name: Get Composer Cache Directory
|
||||||
id: composer-cache
|
id: composer-cache
|
||||||
run: |
|
run: |
|
||||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
@ -47,9 +39,8 @@ jobs:
|
||||||
- name: Lint twig templates
|
- name: Lint twig templates
|
||||||
run: ./bin/console lint:twig templates --env=prod
|
run: ./bin/console lint:twig templates --env=prod
|
||||||
|
|
||||||
# This causes problems with emtpy language files
|
- name: Lint translations
|
||||||
#- name: Lint translations
|
run: ./bin/console lint:xliff translations
|
||||||
# run: ./bin/console lint:xliff translations
|
|
||||||
|
|
||||||
- name: Check dependencies for security
|
- name: Check dependencies for security
|
||||||
uses: symfonycorp/security-checker-action@v5
|
uses: symfonycorp/security-checker-action@v5
|
||||||
|
@ -57,9 +48,8 @@ jobs:
|
||||||
- name: Check doctrine mapping
|
- name: Check doctrine mapping
|
||||||
run: ./bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction
|
run: ./bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction
|
||||||
|
|
||||||
# Use the -d option to raise the max nesting level
|
|
||||||
- name: Generate dev container
|
- name: Generate dev container
|
||||||
run: php -d xdebug.max_nesting_level=1000 ./bin/console cache:clear --env dev
|
run: ./bin/console cache:clear --env dev
|
||||||
|
|
||||||
- name: Run PHPstan
|
- name: Run PHPstan
|
||||||
run: composer phpstan
|
run: composer phpstan
|
||||||
|
|
53
.github/workflows/tests.yml
vendored
53
.github/workflows/tests.yml
vendored
|
@ -13,13 +13,13 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
phpunit:
|
phpunit:
|
||||||
name: PHPUnit and coverage Test (PHP ${{ matrix.php-versions }}, ${{ matrix.db-type }})
|
name: PHPUnit and coverage Test (PHP ${{ matrix.php-versions }}, ${{ matrix.db-type }})
|
||||||
|
# Ubuntu 20.04 ships MySQL 8.0 which causes problems with login, so we just use ubuntu 18.04 for now...
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
php-versions: [ '8.1', '8.2', '8.3', '8.4' ]
|
php-versions: [ '7.4', '8.0', '8.1', '8.2' ]
|
||||||
db-type: [ 'mysql', 'sqlite', 'postgres' ]
|
db-type: [ 'mysql', 'sqlite' ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Note that we set DATABASE URL later based on our db-type matrix value
|
# Note that we set DATABASE URL later based on our db-type matrix value
|
||||||
|
@ -27,43 +27,28 @@ jobs:
|
||||||
SYMFONY_DEPRECATIONS_HELPER: disabled
|
SYMFONY_DEPRECATIONS_HELPER: disabled
|
||||||
PHP_VERSION: ${{ matrix.php-versions }}
|
PHP_VERSION: ${{ matrix.php-versions }}
|
||||||
DB_TYPE: ${{ matrix.db-type }}
|
DB_TYPE: ${{ matrix.db-type }}
|
||||||
CHECK_FOR_UPDATES: false # Disable update checks for tests
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Database env for MySQL
|
- name: Set Database env for MySQL
|
||||||
run: echo "DATABASE_URL=mysql://root:root@127.0.0.1:3306/partdb?serverVersion=8.0.35" >> $GITHUB_ENV
|
run: echo "DATABASE_URL=mysql://root:root@127.0.0.1:3306/test" >> $GITHUB_ENV
|
||||||
if: matrix.db-type == 'mysql'
|
if: matrix.db-type == 'mysql'
|
||||||
|
|
||||||
- name: Set Database env for SQLite
|
- name: Set Database env for SQLite
|
||||||
run: echo "DATABASE_URL="sqlite:///%kernel.project_dir%/var/app_test.db"" >> $GITHUB_ENV
|
run: echo "DATABASE_URL="sqlite:///%kernel.project_dir%/var/app_test.db"" >> $GITHUB_ENV
|
||||||
if: matrix.db-type == 'sqlite'
|
if: matrix.db-type == 'sqlite'
|
||||||
|
|
||||||
- name: Set Database env for PostgreSQL
|
|
||||||
run: echo "DATABASE_URL=postgresql://postgres:postgres @127.0.0.1:5432/partdb?serverVersion=14&charset=utf8" >> $GITHUB_ENV
|
|
||||||
if: matrix.db-type == 'postgres'
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-versions }}
|
php-version: ${{ matrix.php-versions }}
|
||||||
coverage: pcov
|
coverage: pcov
|
||||||
ini-values: xdebug.max_nesting_level=1000
|
extensions: mbstring, intl, gd, xsl, gmp, bcmath
|
||||||
extensions: mbstring, intl, gd, xsl, gmp, bcmath, :php-psr
|
|
||||||
|
|
||||||
- name: Start MySQL
|
- name: Start MySQL
|
||||||
run: sudo systemctl start mysql.service
|
run: sudo systemctl start mysql.service
|
||||||
if: matrix.db-type == 'mysql'
|
|
||||||
|
|
||||||
# Replace the scram-sha-256 with trust for host connections, to avoid password authentication
|
|
||||||
- name: Start PostgreSQL
|
|
||||||
run: |
|
|
||||||
sudo sed -i 's/^\(host.*all.*all.*\)scram-sha-256/\1trust/' /etc/postgresql/14/main/pg_hba.conf
|
|
||||||
sudo systemctl start postgresql.service
|
|
||||||
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"
|
|
||||||
if: matrix.db-type == 'postgres'
|
|
||||||
|
|
||||||
#- name: Setup MySQL
|
#- name: Setup MySQL
|
||||||
# uses: mirromutth/mysql-action@v1.1
|
# uses: mirromutth/mysql-action@v1.1
|
||||||
|
@ -78,7 +63,7 @@ jobs:
|
||||||
id: composer-cache
|
id: composer-cache
|
||||||
run: |
|
run: |
|
||||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
@ -89,7 +74,7 @@ jobs:
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v3
|
||||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
@ -101,7 +86,7 @@ jobs:
|
||||||
run: composer install --prefer-dist --no-progress
|
run: composer install --prefer-dist --no-progress
|
||||||
|
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
|
|
||||||
|
@ -113,24 +98,26 @@ jobs:
|
||||||
|
|
||||||
- name: Create DB
|
- name: Create DB
|
||||||
run: php bin/console --env test doctrine:database:create --if-not-exists -n
|
run: php bin/console --env test doctrine:database:create --if-not-exists -n
|
||||||
if: matrix.db-type == 'mysql' || matrix.db-type == 'postgres'
|
if: matrix.db-type == 'mysql'
|
||||||
|
|
||||||
|
# Checkinf for existance is not supported for sqlite, so do it without it
|
||||||
|
- name: Create DB
|
||||||
|
run: php bin/console --env test doctrine:database:create -n
|
||||||
|
if: matrix.db-type == 'sqlite'
|
||||||
|
|
||||||
- name: Do migrations
|
- name: Do migrations
|
||||||
run: php bin/console --env test doctrine:migrations:migrate -n
|
run: php bin/console --env test doctrine:migrations:migrate -n
|
||||||
|
|
||||||
# Use our own custom fixtures loading command to circumvent some problems with reset the autoincrement values
|
|
||||||
- name: Load fixtures
|
- name: Load fixtures
|
||||||
run: php bin/console --env test partdb:fixtures:load -n
|
run: php bin/console --env test doctrine:fixtures:load -n --purger reset_autoincrement_purger
|
||||||
|
|
||||||
- name: Run PHPunit and generate coverage
|
- name: Run PHPunit and generate coverage
|
||||||
run: ./bin/phpunit --coverage-clover=coverage.xml
|
run: ./bin/phpunit --coverage-clover=coverage.xml
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
env_vars: PHP_VERSION,DB_TYPE
|
env_vars: PHP_VERSION,DB_TYPE
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
fail_ci_if_error: true
|
|
||||||
|
|
||||||
- name: Test app:clean-attachments
|
- name: Test app:clean-attachments
|
||||||
run: php bin/console partdb:attachments:clean-unused -n
|
run: php bin/console partdb:attachments:clean-unused -n
|
||||||
|
@ -144,11 +131,11 @@ jobs:
|
||||||
- name: Test check-requirements command
|
- name: Test check-requirements command
|
||||||
run: php bin/console partdb:check-requirements -n
|
run: php bin/console partdb:check-requirements -n
|
||||||
|
|
||||||
- name: Test partdb:backup command
|
|
||||||
run: php bin/console partdb:backup -n --full /tmp/test_backup.zip
|
|
||||||
|
|
||||||
- name: Test legacy Part-DB import
|
- name: Test legacy Part-DB import
|
||||||
run: bash .github/assets/legacy_import/test_legacy_import.sh
|
run: bash .github/assets/legacy_import/test_legacy_import.sh
|
||||||
if: matrix.db-type == 'mysql' && matrix.php-versions == '8.2'
|
if: matrix.db-type == 'mysql' && matrix.php-versions == '8.2'
|
||||||
env:
|
env:
|
||||||
DATABASE_URL: mysql://root:root@localhost:3306/legacy_db
|
DATABASE_URL: mysql://root:root@localhost:3306/legacy_db
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -43,7 +43,3 @@ yarn-error.log
|
||||||
/phpunit.xml
|
/phpunit.xml
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
###< phpunit/phpunit ###
|
###< phpunit/phpunit ###
|
||||||
|
|
||||||
###> phpstan/phpstan ###
|
|
||||||
phpstan.neon
|
|
||||||
###< phpstan/phpstan ###
|
|
||||||
|
|
178
Dockerfile
178
Dockerfile
|
@ -1,64 +1,22 @@
|
||||||
ARG BASE_IMAGE=debian:bookworm-slim
|
FROM debian:bullseye-slim
|
||||||
ARG PHP_VERSION=8.3
|
|
||||||
|
|
||||||
FROM ${BASE_IMAGE} AS base
|
|
||||||
ARG PHP_VERSION
|
|
||||||
|
|
||||||
# Install needed dependencies for PHP build
|
# Install needed dependencies for PHP build
|
||||||
#RUN apt-get update && apt-get install -y pkg-config curl libcurl4-openssl-dev libicu-dev \
|
#RUN apt-get update && apt-get install -y pkg-config curl libcurl4-openssl-dev libicu-dev \
|
||||||
# libpng-dev libjpeg-dev libfreetype6-dev gnupg zip libzip-dev libjpeg62-turbo-dev libonig-dev libxslt-dev libwebp-dev vim \
|
# libpng-dev libjpeg-dev libfreetype6-dev gnupg zip libzip-dev libjpeg62-turbo-dev libonig-dev libxslt-dev libwebp-dev vim \
|
||||||
# && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
# && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN apt-get update && apt-get -y install \
|
RUN apt-get update && apt-get -y install apt-transport-https lsb-release ca-certificates curl zip \
|
||||||
apt-transport-https \
|
|
||||||
lsb-release \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
zip \
|
|
||||||
mariadb-client \
|
|
||||||
postgresql-client \
|
|
||||||
&& curl -sSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg \
|
&& curl -sSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg \
|
||||||
&& sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' \
|
&& sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' \
|
||||||
&& apt-get update && apt-get upgrade -y \
|
&& apt-get update && apt-get upgrade -y \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y apache2 php8.1 php8.1-fpm php8.1-opcache php8.1-curl php8.1-gd php8.1-mbstring php8.1-xml php8.1-bcmath php8.1-intl php8.1-zip php8.1-xsl php8.1-sqlite3 php8.1-mysql gpg \
|
||||||
apache2 \
|
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
|
||||||
php${PHP_VERSION} \
|
|
||||||
php${PHP_VERSION}-fpm \
|
ENV APACHE_CONFDIR /etc/apache2
|
||||||
php${PHP_VERSION}-opcache \
|
ENV APACHE_ENVVARS $APACHE_CONFDIR/envvars
|
||||||
php${PHP_VERSION}-curl \
|
|
||||||
php${PHP_VERSION}-gd \
|
|
||||||
php${PHP_VERSION}-mbstring \
|
|
||||||
php${PHP_VERSION}-xml \
|
|
||||||
php${PHP_VERSION}-bcmath \
|
|
||||||
php${PHP_VERSION}-intl \
|
|
||||||
php${PHP_VERSION}-zip \
|
|
||||||
php${PHP_VERSION}-xsl \
|
|
||||||
php${PHP_VERSION}-sqlite3 \
|
|
||||||
php${PHP_VERSION}-mysql \
|
|
||||||
php${PHP_VERSION}-pgsql \
|
|
||||||
gpg \
|
|
||||||
sudo \
|
|
||||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* \
|
|
||||||
# Create workdir and set permissions if directory does not exists
|
# Create workdir and set permissions if directory does not exists
|
||||||
&& mkdir -p /var/www/html \
|
RUN mkdir -p /var/www/html && chown -R www-data:www-data /var/www/html
|
||||||
&& chown -R www-data:www-data /var/www/html \
|
|
||||||
# delete the "index.html" that installing Apache drops in here
|
|
||||||
&& rm -rvf /var/www/html/*
|
|
||||||
|
|
||||||
# Install node and yarn
|
|
||||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
|
||||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
|
||||||
curl -sL https://deb.nodesource.com/setup_20.x | bash - && \
|
|
||||||
apt-get update && apt-get install -y \
|
|
||||||
nodejs \
|
|
||||||
yarn \
|
|
||||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install composer
|
|
||||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
|
||||||
|
|
||||||
ENV APACHE_CONFDIR=/etc/apache2
|
|
||||||
ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars
|
|
||||||
|
|
||||||
# Configure apache 2 (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/Dockerfile)
|
# Configure apache 2 (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/Dockerfile)
|
||||||
# generically convert lines like
|
# generically convert lines like
|
||||||
|
@ -69,94 +27,78 @@ ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars
|
||||||
# so that they can be overridden at runtime ("-e APACHE_RUN_USER=...")
|
# so that they can be overridden at runtime ("-e APACHE_RUN_USER=...")
|
||||||
RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"; \
|
RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"; \
|
||||||
set -eux; . "$APACHE_ENVVARS"; \
|
set -eux; . "$APACHE_ENVVARS"; \
|
||||||
|
# delete the "index.html" that installing Apache drops in here
|
||||||
|
rm -rvf /var/www/html/*; \
|
||||||
\
|
\
|
||||||
# logs should go to stdout / stderr
|
# logs should go to stdout / stderr
|
||||||
ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \
|
ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \
|
||||||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log"; \
|
ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log"; \
|
||||||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \
|
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \
|
||||||
|
ln -sfT /dev/stderr /var/log/php8.1-fpm.log; \
|
||||||
chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR";
|
chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR";
|
||||||
|
|
||||||
# ---
|
# Enable php-fpm
|
||||||
|
RUN a2enmod proxy_fcgi setenvif && a2enconf php8.1-fpm
|
||||||
FROM scratch AS apache-config
|
|
||||||
ARG PHP_VERSION
|
|
||||||
# Configure php-fpm to log to stdout of the container (stdout of PID 1)
|
|
||||||
# We have to use /proc/1/fd/1 because /dev/stdout or /proc/self/fd/1 does not point to the container stdout (because we use apache as entrypoint)
|
|
||||||
# We also disable the clear_env option to allow the use of environment variables in php-fpm
|
|
||||||
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/pool.d/zz-docker.conf
|
|
||||||
[global]
|
|
||||||
error_log = /proc/1/fd/1
|
|
||||||
|
|
||||||
[www]
|
|
||||||
access.log = /proc/1/fd/1
|
|
||||||
catch_workers_output = yes
|
|
||||||
decorate_workers_output = no
|
|
||||||
clear_env = no
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# PHP files should be handled by PHP, and should be preferred over any other file type
|
# PHP files should be handled by PHP, and should be preferred over any other file type
|
||||||
COPY <<EOF /etc/apache2/conf-available/docker-php.conf
|
RUN { \
|
||||||
<FilesMatch \\.php$>
|
echo '<FilesMatch \.php$>'; \
|
||||||
SetHandler application/x-httpd-php
|
echo '\tSetHandler application/x-httpd-php'; \
|
||||||
</FilesMatch>
|
echo '</FilesMatch>'; \
|
||||||
|
echo; \
|
||||||
DirectoryIndex disabled
|
echo 'DirectoryIndex disabled'; \
|
||||||
DirectoryIndex index.php index.html
|
echo 'DirectoryIndex index.php index.html'; \
|
||||||
|
echo; \
|
||||||
<Directory /var/www/>
|
echo '<Directory /var/www/>'; \
|
||||||
Options -Indexes
|
echo '\tOptions -Indexes'; \
|
||||||
AllowOverride All
|
echo '\tAllowOverride All'; \
|
||||||
</Directory>
|
echo '</Directory>'; \
|
||||||
EOF
|
} | tee "$APACHE_CONFDIR/conf-available/docker-php.conf" \
|
||||||
|
&& a2enconf docker-php
|
||||||
|
|
||||||
# Enable opcache and configure it recommended for symfony (see https://symfony.com/doc/current/performance.html)
|
# Enable opcache and configure it recommended for symfony (see https://symfony.com/doc/current/performance.html)
|
||||||
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/conf.d/symfony-recommended.ini
|
RUN \
|
||||||
opcache.memory_consumption=256
|
{ \
|
||||||
opcache.max_accelerated_files=20000
|
echo 'opcache.memory_consumption=256'; \
|
||||||
opcache.validate_timestamp=0
|
echo 'opcache.max_accelerated_files=20000'; \
|
||||||
|
echo 'opcache.validate_timestamp=0'; \
|
||||||
# Configure Realpath cache for performance
|
# Configure Realpath cache for performance
|
||||||
realpath_cache_size=4096K
|
echo 'realpath_cache_size=4096K'; \
|
||||||
realpath_cache_ttl=600
|
echo 'realpath_cache_ttl=600'; \
|
||||||
EOF
|
} > /etc/php/8.1/fpm/conf.d/symfony-recommended.ini
|
||||||
|
|
||||||
# Increase upload limit and enable preloading
|
# Increase upload limit and enable preloading
|
||||||
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/conf.d/partdb.ini
|
RUN \
|
||||||
upload_max_filesize=256M
|
{ \
|
||||||
post_max_size=300M
|
echo 'upload_max_filesize=256M'; \
|
||||||
opcache.preload_user=www-data
|
echo 'post_max_size=300M'; \
|
||||||
opcache.preload=/var/www/html/config/preload.php
|
echo 'opcache.preload_user=www-data'; \
|
||||||
log_limit=8096
|
echo 'opcache.preload=/var/www/html/config/preload.php'; \
|
||||||
EOF
|
} > /etc/php/8.1/fpm/conf.d/partdb.ini
|
||||||
|
|
||||||
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf
|
# Install node and yarn
|
||||||
|
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||||
|
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||||
|
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - && apt-get update && apt-get install -y nodejs yarn && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ---
|
# Install composer
|
||||||
|
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
FROM base
|
|
||||||
ARG PHP_VERSION
|
|
||||||
|
|
||||||
# Set working dir
|
# Set working dir
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
COPY --from=apache-config / /
|
|
||||||
COPY --chown=www-data:www-data . .
|
COPY --chown=www-data:www-data . .
|
||||||
|
|
||||||
# Setup apache2
|
# Setup apache2
|
||||||
RUN a2dissite 000-default.conf && \
|
RUN a2dissite 000-default.conf
|
||||||
a2ensite symfony.conf && \
|
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf
|
||||||
# Enable php-fpm
|
RUN a2ensite symfony.conf
|
||||||
a2enmod proxy_fcgi setenvif && \
|
RUN a2enmod rewrite
|
||||||
a2enconf php${PHP_VERSION}-fpm && \
|
|
||||||
a2enconf docker-php && \
|
|
||||||
a2enmod rewrite
|
|
||||||
|
|
||||||
# Install composer and yarn dependencies for Part-DB
|
# Install composer and yarn dependencies for Part-DB
|
||||||
USER www-data
|
USER www-data
|
||||||
RUN composer install -a --no-dev && \
|
RUN composer install -a --no-dev && composer clear-cache
|
||||||
composer clear-cache
|
RUN yarn install --network-timeout 600000 && yarn build && yarn cache clean && rm -rf node_modules/
|
||||||
RUN yarn install --network-timeout 600000 && \
|
|
||||||
yarn build && \
|
|
||||||
yarn cache clean && \
|
|
||||||
rm -rf node_modules/
|
|
||||||
|
|
||||||
# Use docker env to output logs to stdout
|
# Use docker env to output logs to stdout
|
||||||
ENV APP_ENV=docker
|
ENV APP_ENV=docker
|
||||||
|
@ -164,12 +106,10 @@ ENV DATABASE_URL="sqlite:///%kernel.project_dir%/uploads/app.db"
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
# Replace the php version placeholder in the entry point, with our php version
|
# Copy entrypoint to /usr/local/bin and make it executable
|
||||||
RUN sed -i "s/PHP_VERSION/${PHP_VERSION}/g" ./.docker/partdb-entrypoint.sh
|
RUN cp ./.docker/partdb-entrypoint.sh /usr/local/bin/partdb-entrypoint.sh && chmod +x /usr/local/bin/partdb-entrypoint.sh
|
||||||
|
# Copy apache2-foreground to /usr/local/bin and make it executable
|
||||||
# Copy entrypoint and apache2-foreground to /usr/local/bin and make it executable
|
RUN cp ./.docker/apache2-foreground /usr/local/bin/apache2-foreground && chmod +x /usr/local/bin/apache2-foreground
|
||||||
RUN install ./.docker/partdb-entrypoint.sh /usr/local/bin && \
|
|
||||||
install ./.docker/apache2-foreground /usr/local/bin
|
|
||||||
ENTRYPOINT ["partdb-entrypoint.sh"]
|
ENTRYPOINT ["partdb-entrypoint.sh"]
|
||||||
CMD ["apache2-foreground"]
|
CMD ["apache2-foreground"]
|
||||||
|
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get -y install \
|
|
||||||
curl \
|
|
||||||
ca-certificates \
|
|
||||||
mariadb-client \
|
|
||||||
postgresql-client \
|
|
||||||
file \
|
|
||||||
acl \
|
|
||||||
git \
|
|
||||||
gettext \
|
|
||||||
gnupg \
|
|
||||||
zip \
|
|
||||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
|
|
||||||
|
|
||||||
# Install node and yarn
|
|
||||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
|
||||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
|
||||||
curl -sL https://deb.nodesource.com/setup_20.x | bash - && \
|
|
||||||
apt-get update && apt-get install -y \
|
|
||||||
nodejs yarn \
|
|
||||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install PHP
|
|
||||||
RUN set -eux; \
|
|
||||||
install-php-extensions \
|
|
||||||
@composer \
|
|
||||||
apcu \
|
|
||||||
intl \
|
|
||||||
opcache \
|
|
||||||
zip \
|
|
||||||
pdo_mysql \
|
|
||||||
pdo_sqlite \
|
|
||||||
pdo_pgsql \
|
|
||||||
gd \
|
|
||||||
bcmath \
|
|
||||||
xsl \
|
|
||||||
;
|
|
||||||
|
|
||||||
# Copy config files for php and caddy
|
|
||||||
COPY --link .docker/frankenphp/conf.d/app.ini $PHP_INI_DIR/conf.d/
|
|
||||||
COPY --chmod=755 .docker/frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
|
|
||||||
COPY --link .docker/frankenphp/Caddyfile /etc/caddy/Caddyfile
|
|
||||||
COPY --link .docker/frankenphp/conf.d/app.prod.ini $PHP_INI_DIR/conf.d/
|
|
||||||
COPY --link .docker/frankenphp/worker.Caddyfile /etc/caddy/worker.Caddyfile
|
|
||||||
ENV FRANKENPHP_CONFIG="import worker.Caddyfile"
|
|
||||||
|
|
||||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
|
||||||
|
|
||||||
# Install composer
|
|
||||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
|
||||||
#COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
|
||||||
|
|
||||||
# Create workdir and set permissions if directory does not exists
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# prevent the reinstallation of vendors at every changes in the source code
|
|
||||||
COPY --link composer.* symfony.* ./
|
|
||||||
RUN set -eux; \
|
|
||||||
composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress
|
|
||||||
|
|
||||||
# copy sources
|
|
||||||
COPY --link . ./
|
|
||||||
|
|
||||||
# Install composer and yarn dependencies for Part-DB
|
|
||||||
RUN set -eux; \
|
|
||||||
mkdir -p var/cache var/log; \
|
|
||||||
composer dump-autoload --classmap-authoritative --no-dev; \
|
|
||||||
composer dump-env prod; \
|
|
||||||
composer run-script --no-dev post-install-cmd; \
|
|
||||||
chmod +x bin/console; sync;
|
|
||||||
|
|
||||||
RUN yarn install --network-timeout 600000 && \
|
|
||||||
yarn build && \
|
|
||||||
yarn cache clean && \
|
|
||||||
rm -rf node_modules/
|
|
||||||
|
|
||||||
# Use docker env to output logs to stdout
|
|
||||||
ENV APP_ENV=docker
|
|
||||||
ENV DATABASE_URL="sqlite:///%kernel.project_dir%/uploads/app.db"
|
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
ENTRYPOINT ["docker-entrypoint"]
|
|
||||||
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
|
|
||||||
|
|
||||||
# https://httpd.apache.org/docs/2.4/stopping.html#gracefulstop
|
|
||||||
STOPSIGNAL SIGWINCH
|
|
||||||
|
|
||||||
VOLUME ["/var/www/html/uploads", "/var/www/html/public/media"]
|
|
||||||
|
|
||||||
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
|
|
||||||
|
|
||||||
# See https://caddyserver.com/docs/conventions#file-locations for details
|
|
||||||
ENV XDG_CONFIG_HOME /config
|
|
||||||
ENV XDG_DATA_HOME /data
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 443
|
|
||||||
EXPOSE 443/udp
|
|
||||||
EXPOSE 2019
|
|
141
README.md
141
README.md
|
@ -1,164 +1,127 @@
|
||||||
[](https://scrutinizer-ci.com/g/Part-DB/Part-DB-symfony/?branch=master)
|
[](https://scrutinizer-ci.com/g/Part-DB/Part-DB-symfony/?branch=master)
|
||||||

|

|
||||||

|

|
||||||
[](https://codecov.io/gh/Part-DB/Part-DB-server)
|
[](https://codecov.io/gh/Part-DB/Part-DB-server)
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
[](https://part-db.crowdin.com/part-db)
|
[](https://part-db.crowdin.com/part-db)
|
||||||
|
|
||||||
**[Documentation](https://docs.part-db.de/)** | **[Demo](https://demo.part-db.de/)** | **[Docker Image](https://hub.docker.com/r/jbtronics/part-db1)**
|
**[Documentation](https://docs.part-db.de/)** | **[Demo](https://part-db.herokuapp.com)** | **[Docker Image](https://hub.docker.com/r/jbtronics/part-db1)**
|
||||||
|
|
||||||
# Part-DB
|
# Part-DB
|
||||||
|
Part-DB is an Open-Source inventory managment system for your electronic components.
|
||||||
Part-DB is an Open-Source inventory management system for your electronic components.
|
|
||||||
It is installed on a web server and so can be accessed with any browser without the need to install additional software.
|
It is installed on a web server and so can be accessed with any browser without the need to install additional software.
|
||||||
|
|
||||||
The version in this repository is a complete rewrite of the legacy [Part-DB](https://github.com/Part-DB/Part-DB)
|
The version in this Repository is a complete rewrite of the legacy [Part-DB](https://github.com/Part-DB/Part-DB) (Version < 1.0) based on a modern framework.
|
||||||
(Version < 1.0) based on a modern framework and is the recommended version to use.
|
Currently, it is still missing some (minor) features from the old version (see [UPGRADE.md](https://docs.part-db.de/upgrade_legacy.html)) for more details, but also many huge improvements and advantages compared to the old version.
|
||||||
|
If you start completely new with Part-DB it is recommended that you use the version from this repository, as it is actively developed.
|
||||||
|
|
||||||
If you find a bug, please open an [Issue on GitHub,](https://github.com/Part-DB/Part-DB-server/issues) so it can be fixed
|
If you find a bug, please open an [Issue on Github](https://github.com/Part-DB/Part-DB-server/issues) so it can be fixed for everybody.
|
||||||
for everybody.
|
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
If you want to test Part-DB without installing it, you can use [this](https://part-db.herokuapp.com) Heroku instance.
|
||||||
If you want to test Part-DB without installing it, you can use [this](https://demo.part-db.de/) Heroku instance.
|
(Or this link for the [German Version](https://part-db.herokuapp.com/de/)).
|
||||||
(Or this link for the [German Version](https://demo.part-db.de/de/)).
|
|
||||||
|
|
||||||
You can log in with username: *user* and password: *user*.
|
You can log in with username: *user* and password: *user*.
|
||||||
|
|
||||||
Every change to the master branch gets automatically deployed, so it represents the current development progress and is
|
Every change to the master branch gets automatically deployed, so it represents the current development progress and is
|
||||||
may not completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading
|
maybe not completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading the page
|
||||||
the page
|
|
||||||
for the first time.
|
for the first time.
|
||||||
|
|
||||||
<img src="https://github.com/Part-DB/Part-DB-server/raw/master/docs/assets/readme/part_info.png">
|
<img src="https://github.com/Part-DB/Part-DB-server/raw/master/docs/assets/readme/part_info.png">
|
||||||
<img src="https://github.com/Part-DB/Part-DB-server/raw/master/docs/assets/readme/parts_list.png">
|
<img src="https://github.com/Part-DB/Part-DB-server/raw/master/docs/assets/readme/parts_list.png">
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
* Inventory management of your electronic parts. Each part can be assigned to a category, footprint, manufacturer
|
||||||
* Inventory management of your electronic parts. Each part can be assigned to a category, footprint, manufacturer,
|
and multiple store locations and price information. Parts can be grouped using tags. You can associate various files like datasheets or pictures with the parts.
|
||||||
and multiple store locations and price information. Parts can be grouped using tags. You can associate various files
|
* Multi-Language support (currently German, English, Russian, Japanese and French (experimental))
|
||||||
like datasheets or pictures with the parts.
|
|
||||||
* Multi-language support (currently German, English, Russian, Japanese, French, Czech, Danish, and Chinese)
|
|
||||||
* Barcodes/Labels generator for parts and storage locations, scan barcodes via webcam using the builtin barcode scanner
|
* Barcodes/Labels generator for parts and storage locations, scan barcodes via webcam using the builtin barcode scanner
|
||||||
* User system with groups and detailed (fine granular) permissions.
|
* User system with groups and detailed (fine granular) permissions.
|
||||||
Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups.
|
Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups. Password reset via email can be setuped.
|
||||||
Password reset via email can be set up.
|
* Optional support for single sign-on (SSO) via SAML (using an intermediate service like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server)
|
||||||
* Optional support for single sign-on (SSO) via SAML (using an intermediate service
|
|
||||||
like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server)
|
|
||||||
* Import/Export system for parts and datastructure. BOM import for projects from KiCAD is supported.
|
* Import/Export system for parts and datastructure. BOM import for projects from KiCAD is supported.
|
||||||
* Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build
|
* Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build this project and directly withdraw all components needed from DB
|
||||||
this project and directly withdraw all components needed from DB
|
* Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older versions.
|
||||||
* Event log: Track what changes happen to your inventory, track which user does what. Revert your parts to older
|
* Responsive design: You can use Part-DB on your PC, your tablet and your smartphone using the same interface.
|
||||||
versions.
|
* MySQL and SQLite supported as database backends
|
||||||
* Responsive design: You can use Part-DB on your PC, your tablet, and your smartphone using the same interface.
|
|
||||||
* MySQL, SQLite and PostgreSQL are supported as database backends
|
|
||||||
* Support for rich text descriptions and comments in parts
|
* Support for rich text descriptions and comments in parts
|
||||||
* Support for multiple currencies and automatic update of exchange rates supported
|
* Support for multiple currencies and automatic update of exchange rates supported
|
||||||
* Powerful search and filter function, including parametric search (search for parts according to some specifications)
|
* Powerful search and filter function, including parametric search (search for parts according to some specifications)
|
||||||
* Automatic thumbnail generation for pictures
|
* Automatic thumbnail generation for pictures
|
||||||
* Use cloud providers (like Octopart, Digikey, Farnell, LCSC or TME) to automatically get part information, datasheets, and
|
|
||||||
prices for parts
|
|
||||||
* API to access Part-DB from other applications/scripts
|
|
||||||
* [Integration with KiCad](https://docs.part-db.de/usage/eda_integration.html): Use Part-DB as the central datasource for your
|
|
||||||
KiCad and see available parts from Part-DB directly inside KiCad.
|
|
||||||
|
|
||||||
With these features, Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory,
|
|
||||||
or maker spaces, where many users should have (controlled) access to the shared inventory.
|
With these features Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory,
|
||||||
|
or makerspaces, where many users have should have (controlled) access to the shared inventory.
|
||||||
|
|
||||||
Part-DB is also used by small companies and universities for managing their inventory.
|
Part-DB is also used by small companies and universities for managing their inventory.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
* A **web server** (like Apache2 or nginx) that is capable of running [Symfony 5](https://symfony.com/doc/current/reference/requirements.html),
|
||||||
* A **web server** (like Apache2 or nginx) that is capable of
|
this includes a minimum PHP version of **PHP 7.4**
|
||||||
running [Symfony 6](https://symfony.com/doc/current/reference/requirements.html),
|
* A **MySQL** (at least 5.7) /**MariaDB** (at least 10.2.2) database server if you do not want to use SQLite.
|
||||||
this includes a minimum PHP version of **PHP 8.1**
|
* Shell access to your server is highly suggested!
|
||||||
* A **MySQL** (at least 5.7) /**MariaDB** (at least 10.4) database server, or **PostgreSQL** 10+ if you do not want to use SQLite.
|
* For building the client side assets **yarn** and **nodejs** is needed.
|
||||||
* Shell access to your server is highly recommended!
|
|
||||||
* For building the client-side assets **yarn** and **nodejs** (>= 18.0) is needed.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
If you want to upgrade your legacy (< 1.0.0) version of Part-DB to this version, please read [this](https://docs.part-db.de/upgrade_legacy.html) first.
|
||||||
|
|
||||||
If you want to upgrade your legacy (< 1.0.0) version of Part-DB to this version, please
|
*Hint:* A docker image is available under [jbtronics/part-db1](https://hub.docker.com/r/jbtronics/part-db1). How to set up Part-DB via docker is described [here](https://docs.part-db.de/installation/installation_docker.html).
|
||||||
read [this](https://docs.part-db.de/upgrade_legacy.html) first.
|
|
||||||
|
|
||||||
*Hint:* A docker image is available under [jbtronics/part-db1](https://hub.docker.com/r/jbtronics/part-db1). How to set
|
**Below you find some very rough outline of the installation process, see [here](https://docs.part-db.de/installation/) for a detailed guide how to install Part-DB.**
|
||||||
up Part-DB via docker is described [here](https://docs.part-db.de/installation/installation_docker.html).
|
|
||||||
|
|
||||||
**Below you find a very rough outline of the installation process, see [here](https://docs.part-db.de/installation/)
|
|
||||||
for a detailed guide on how to install Part-DB.**
|
|
||||||
|
|
||||||
1. Copy or clone this repository into a folder on your server.
|
1. Copy or clone this repository into a folder on your server.
|
||||||
2. Configure your webserver to serve from the `public/` folder.
|
2. Configure your webserver to serve from the `public/` folder. See [here](https://symfony.com/doc/current/setup/web_server_configuration.html)
|
||||||
See [here](https://symfony.com/doc/current/setup/web_server_configuration.html)
|
|
||||||
for additional information.
|
for additional information.
|
||||||
3. Copy the global config file `cp .env .env.local` and edit `.env.local`:
|
3. Copy the global config file `cp .env .env.local` and edit `.env.local`:
|
||||||
* Change the line `APP_ENV=dev` to `APP_ENV=prod`
|
* Change the line `APP_ENV=dev` to `APP_ENV=prod`
|
||||||
* If you do not want to use SQLite, change the value of `DATABASE_URL=` to your needs (
|
* If you do not want to use SQLite, change the value of `DATABASE_URL=` to your needs (see [here](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url)) for the format.
|
||||||
see [here](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url))
|
In bigger instances with concurrent accesses, MySQL is more performant. This can not be changed easily later, so choose wisely.
|
||||||
for the format.
|
|
||||||
In bigger instances with concurrent accesses, MySQL is more performant. This can not be changed easily later, so
|
|
||||||
choose wisely.
|
|
||||||
4. Install composer dependencies and generate autoload files: `composer install -o --no-dev`
|
4. Install composer dependencies and generate autoload files: `composer install -o --no-dev`
|
||||||
5. Install client side dependencies and build it: `yarn install` and `yarn build`
|
5. If you have put Part-DB into a sub-directory on your server (like `part-db/`), you have to edit the file
|
||||||
6. _Optional_ (speeds up first load): Warmup cache: `php bin/console cache:warmup`
|
`webpack.config.js` and uncomment the lines (remove the `//` before the lines) `.setPublicPath('/part-db/build')` (line 43) and
|
||||||
7. Upgrade database to new scheme (or create it, when it was empty): `php bin/console doctrine:migrations:migrate` and
|
`.setManifestKeyPrefix('build/')` (line 44). You have to replace `/part-db` with your own path on line 44.
|
||||||
follow the instructions given. During the process the password for the admin is user is shown. Copy it. **Caution**:
|
6. Install client side dependencies and build it: `yarn install` and `yarn build`
|
||||||
These steps tamper with your database and could potentially destroy it. So make sure to make a backup of your
|
7. _Optional_ (speeds up first load): Warmup cache: `php bin/console cache:warmup`
|
||||||
database.
|
8. Upgrade database to new scheme (or create it, when it was empty): `php bin/console doctrine:migrations:migrate` and follow the instructions given. During the process the password for the admin is user is shown. Copy it. **Caution**: This steps tamper with your database and could potentially destroy it. So make sure to make a backup of your database.
|
||||||
8. You can configure Part-DB via `config/parameters.yaml`. You should check if settings match your expectations after
|
9. You can configure Part-DB via `config/parameters.yaml`. You should check if settings match your expectations, after you installed/upgraded Part-DB. Check if `partdb.default_currency` matches your mainly used currency (this can not be changed after creating price informations).
|
||||||
you installed/upgraded Part-DB. Check if `partdb.default_currency` matches your mainly used currency (this can not be
|
Run `php bin/console cache:clear` when you changed something.
|
||||||
changed after creating price information).
|
10. Access Part-DB in your browser (under the URL you put it) and login with user *admin*. Password is the one outputted during DB setup.
|
||||||
Run `php bin/console cache:clear` when you change something.
|
If you can not remember the password, set a new one with `php bin/console app:set-password admin`. You can create new users with the admin user and start using Part-DB.
|
||||||
9. Access Part-DB in your browser (under the URL you put it) and log in with user *admin*. Password is the one outputted
|
|
||||||
during DB setup.
|
|
||||||
If you can not remember the password, set a new one with `php bin/console app:set-password admin`. You can create
|
|
||||||
new users with the admin user and start using Part-DB.
|
|
||||||
|
|
||||||
When you want to upgrade to a newer version, then just copy the new files into the folder
|
When you want to upgrade to a newer version, then just copy the new files into the folder
|
||||||
and repeat the steps 4. to 7.
|
and repeat the steps 4. to 7.
|
||||||
|
|
||||||
Normally a random password is generated when the admin user is created during initial database creation,
|
Normally a random password is generated when the admin user is created during inital database creation,
|
||||||
however, you can set the initial admin password, by setting the `INITIAL_ADMIN_PW` env var.
|
however you can set the inital admin password, by setting the `INITIAL_ADMIN_PW` env var.
|
||||||
|
|
||||||
You can configure Part-DB to your needs by changing environment variables in the `.env.local` file.
|
You can configure Part-DB to your needs by changing environment variables in the `.env.local` file.
|
||||||
See [here](https://docs.part-db.de/configuration.html) for more information.
|
See [here](https://docs.part-db.de/configuration.html) for more information.
|
||||||
|
|
||||||
### Reverse proxy
|
### Reverse proxy
|
||||||
|
If you are using a reverse proxy, you have to ensure that the proxies sets the `X-Forwarded-*` headers correctly, or you will get HTTP/HTTPS mixup and wrong hostnames.
|
||||||
If you are using a reverse proxy, you have to ensure that the proxies set the `X-Forwarded-*` headers correctly, or you
|
If the reverse proxy is on a different server (or it cannot access Part-DB via localhost) you have to set the `TRUSTED_PROXIES` env variable to match your reverse proxies IP-address (or IP block). You can do this in your `.env.local` or (when using docker) in your `docker-compose.yml` file.
|
||||||
will get HTTP/HTTPS mixup and wrong hostnames.
|
|
||||||
If the reverse proxy is on a different server (or it cannot access Part-DB via localhost) you have to set
|
|
||||||
the `TRUSTED_PROXIES` env variable to match your reverse proxy's IP address (or IP block). You can do this in
|
|
||||||
your `.env.local` or (when using docker) in your `docker-compose.yml` file.
|
|
||||||
|
|
||||||
## Donate for development
|
## Donate for development
|
||||||
|
|
||||||
If you want to donate to the Part-DB developer, see the sponsor button in the top bar (next to the repo name).
|
If you want to donate to the Part-DB developer, see the sponsor button in the top bar (next to the repo name).
|
||||||
There you will find various methods to support development on a monthly or a one-time base.
|
There you will find various methods to support development on a monthly or a one time base.
|
||||||
|
|
||||||
## Built with
|
## Built with
|
||||||
|
|
||||||
* [Symfony 5](https://symfony.com/): The main framework used for the serverside PHP
|
* [Symfony 5](https://symfony.com/): The main framework used for the serverside PHP
|
||||||
* [Bootstrap 5](https://getbootstrap.com/) and [Bootswatch](https://bootswatch.com/): Used as website theme
|
* [Bootstrap 5](https://getbootstrap.com/) and [Bootswatch](https://bootswatch.com/): Used as website theme
|
||||||
* [Fontawesome](https://fontawesome.com/): Used as icon set
|
* [Fontawesome](https://fontawesome.com/): Used as icon set
|
||||||
* [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend
|
* [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend Javascript
|
||||||
Javascript
|
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
* **Jan Böhmer** - *Inital work* - [Github](https://github.com/jbtronics/)
|
||||||
|
|
||||||
* **Jan Böhmer** - *Initial work* - [GitHub](https://github.com/jbtronics/)
|
See also the list of [contributors](https://github.com/Part-DB/Part-DB-server/graphs/contributors) who participated in this project.
|
||||||
|
|
||||||
See also the list of [contributors](https://github.com/Part-DB/Part-DB-server/graphs/contributors) who participated in
|
|
||||||
this project.
|
|
||||||
|
|
||||||
Based on the original Part-DB by Christoph Lechner and K. Jacobs
|
Based on the original Part-DB by Christoph Lechner and K. Jacobs
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Part-DB is licensed under the GNU Affero General Public License v3.0 (or at your opinion any later).
|
Part-DB is licensed under the GNU Affero General Public License v3.0 (or at your opinion any later).
|
||||||
This mostly means that you can use Part-DB for whatever you want (even use it commercially)
|
This mostly means that you can use Part-DB for whatever you want (even use it commercially)
|
||||||
as long as you publish the source code for every change you make under the AGPL, too.
|
as long as you publish the source code for every change you make under the AGPL, too.
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
1.17.1
|
1.3.3
|
||||||
|
|
3
assets/bootstrap.js
vendored
3
assets/bootstrap.js
vendored
|
@ -4,7 +4,8 @@ import { startStimulusApp } from '@symfony/stimulus-bridge';
|
||||||
export const app = startStimulusApp(require.context(
|
export const app = startStimulusApp(require.context(
|
||||||
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
|
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
|
||||||
true,
|
true,
|
||||||
/\.[jt]sx?$/
|
/\.(j|t)sx?$/
|
||||||
));
|
));
|
||||||
|
|
||||||
// register any custom, 3rd party controllers here
|
// register any custom, 3rd party controllers here
|
||||||
// app.register('some_controller_name', SomeImportedController);
|
// app.register('some_controller_name', SomeImportedController);
|
||||||
|
|
|
@ -181,8 +181,7 @@ Editor.defaultConfig = {
|
||||||
'DejaVu Serif, serif',
|
'DejaVu Serif, serif',
|
||||||
'Helvetica, Arial, sans-serif',
|
'Helvetica, Arial, sans-serif',
|
||||||
'Times New Roman, Times, serif',
|
'Times New Roman, Times, serif',
|
||||||
'Courier New, Courier, monospace',
|
'Courier New, Courier, monospace'
|
||||||
'Unifont, monospace',
|
|
||||||
],
|
],
|
||||||
supportAllValues: true
|
supportAllValues: true
|
||||||
},
|
},
|
||||||
|
|
|
@ -76,7 +76,6 @@ const PLACEHOLDERS = [
|
||||||
['[[FOOTPRINT_FULL]]', 'Footprint (Full path)'],
|
['[[FOOTPRINT_FULL]]', 'Footprint (Full path)'],
|
||||||
['[[MASS]]', 'Mass'],
|
['[[MASS]]', 'Mass'],
|
||||||
['[[MPN]]', 'Manufacturer Product Number (MPN)'],
|
['[[MPN]]', 'Manufacturer Product Number (MPN)'],
|
||||||
['[[IPN]]', 'Internal Part Number (IPN)'],
|
|
||||||
['[[TAGS]]', 'Tags'],
|
['[[TAGS]]', 'Tags'],
|
||||||
['[[M_STATUS]]', 'Manufacturing status'],
|
['[[M_STATUS]]', 'Manufacturing status'],
|
||||||
['[[DESCRIPTION]]', 'Description'],
|
['[[DESCRIPTION]]', 'Description'],
|
||||||
|
@ -85,9 +84,6 @@ const PLACEHOLDERS = [
|
||||||
['[[COMMENT_T]]', 'Comment (plain text)'],
|
['[[COMMENT_T]]', 'Comment (plain text)'],
|
||||||
['[[LAST_MODIFIED]]', 'Last modified datetime'],
|
['[[LAST_MODIFIED]]', 'Last modified datetime'],
|
||||||
['[[CREATION_DATE]]', 'Creation datetime'],
|
['[[CREATION_DATE]]', 'Creation datetime'],
|
||||||
['[[IPN_BARCODE_QR]]', 'IPN as QR code'],
|
|
||||||
['[[IPN_BARCODE_C128]]', 'IPN as Code 128 barcode'],
|
|
||||||
['[[IPN_BARCODE_C39]]', 'IPN as Code 39 barcode'],
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -128,8 +124,6 @@ const PLACEHOLDERS = [
|
||||||
['[[BARCODE_QR]]', 'QR code linking to this element'],
|
['[[BARCODE_QR]]', 'QR code linking to this element'],
|
||||||
['[[BARCODE_C128]]', 'Code 128 barcode linking to this element'],
|
['[[BARCODE_C128]]', 'Code 128 barcode linking to this element'],
|
||||||
['[[BARCODE_C39]]', 'Code 39 barcode linking to this element'],
|
['[[BARCODE_C39]]', 'Code 39 barcode linking to this element'],
|
||||||
['[[BARCODE_C93]]', 'Code 93 barcode linking to this element'],
|
|
||||||
['[[BARCODE_DATAMATRIX]]', 'Datamatrix code linking to this element'],
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -39,7 +39,6 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
|
||||||
'Footprint (Full path)': 'Footprint (Vollständiger Pfad)',
|
'Footprint (Full path)': 'Footprint (Vollständiger Pfad)',
|
||||||
'Mass': 'Gewicht',
|
'Mass': 'Gewicht',
|
||||||
'Manufacturer Product Number (MPN)': 'Hersteller Produktnummer (MPN)',
|
'Manufacturer Product Number (MPN)': 'Hersteller Produktnummer (MPN)',
|
||||||
'Internal Part Number (IPN)': 'Internal Part Number (IPN)',
|
|
||||||
'Tags': 'Tags',
|
'Tags': 'Tags',
|
||||||
'Manufacturing status': 'Herstellungsstatus',
|
'Manufacturing status': 'Herstellungsstatus',
|
||||||
'Description': 'Beschreibung',
|
'Description': 'Beschreibung',
|
||||||
|
@ -48,9 +47,6 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
|
||||||
'Comment (plain text)': 'Kommentar (Nur-Text)',
|
'Comment (plain text)': 'Kommentar (Nur-Text)',
|
||||||
'Last modified datetime': 'Zuletzt geändert',
|
'Last modified datetime': 'Zuletzt geändert',
|
||||||
'Creation datetime': 'Erstellt',
|
'Creation datetime': 'Erstellt',
|
||||||
'IPN as QR code': 'IPN als QR Code',
|
|
||||||
'IPN as Code 128 barcode': 'IPN als Code 128 Barcode',
|
|
||||||
'IPN as Code 39 barcode': 'IPN als Code 39 Barcode',
|
|
||||||
|
|
||||||
'Lot ID': 'Lot ID',
|
'Lot ID': 'Lot ID',
|
||||||
'Lot name': 'Lot Name',
|
'Lot name': 'Lot Name',
|
||||||
|
@ -69,8 +65,6 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
|
||||||
'QR code linking to this element': 'QR Code verknüpft mit diesem Element',
|
'QR code linking to this element': 'QR Code verknüpft mit diesem Element',
|
||||||
'Code 128 barcode linking to this element': 'Code 128 Barcode verknüpft mit diesem Element',
|
'Code 128 barcode linking to this element': 'Code 128 Barcode verknüpft mit diesem Element',
|
||||||
'Code 39 barcode linking to this element': 'Code 39 Barcode verknüpft mit diesem Element',
|
'Code 39 barcode linking to this element': 'Code 39 Barcode verknüpft mit diesem Element',
|
||||||
'Code 93 barcode linking to this element': 'Code 93 Barcode verknüpft mit diesem Element',
|
|
||||||
'Datamatrix code linking to this element': 'Datamatrix Code verknüpft mit diesem Element',
|
|
||||||
|
|
||||||
'Location ID': 'Lagerort ID',
|
'Location ID': 'Lagerort ID',
|
||||||
'Name': 'Name',
|
'Name': 'Name',
|
||||||
|
|
|
@ -18,118 +18,43 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
import {Controller} from "@hotwired/stimulus";
|
||||||
|
import Darkmode from "darkmode-js/src";
|
||||||
|
import "darkmode-js"
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
|
||||||
|
_darkmode;
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.setMode(this.getMode());
|
if (typeof window.getComputedStyle(document.body).mixBlendMode == 'undefined') {
|
||||||
document.querySelectorAll('input[name="darkmode"]').forEach((radio) => {
|
console.warn("The browser does not support mix blend mode. Darkmode will not work.");
|
||||||
radio.addEventListener('change', this._radioChanged.bind(this));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event listener for the change of radio buttons
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_radioChanged(event) {
|
|
||||||
const new_mode = this.getSelectedMode();
|
|
||||||
this.setMode(new_mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current mode from the local storage
|
|
||||||
* @return {'dark', 'light', 'auto'}
|
|
||||||
*/
|
|
||||||
getMode() {
|
|
||||||
return localStorage.getItem('darkmode') ?? 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the mode in the local storage and apply it and change the state of the radio buttons
|
|
||||||
* @param mode
|
|
||||||
*/
|
|
||||||
setMode(mode) {
|
|
||||||
if (mode !== 'dark' && mode !== 'light' && mode !== 'auto') {
|
|
||||||
console.warn('Invalid darkmode mode: ' + mode);
|
|
||||||
mode = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('darkmode', mode);
|
|
||||||
|
|
||||||
this.setButtonMode(mode);
|
|
||||||
|
|
||||||
if (mode === 'auto') {
|
|
||||||
this._setDarkmodeAuto();
|
|
||||||
} else if (mode === 'dark') {
|
|
||||||
this._enableDarkmode();
|
|
||||||
} else if (mode === 'light') {
|
|
||||||
this._disableDarkmode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the selected mode via the radio buttons
|
|
||||||
* @return {'dark', 'light', 'auto'}
|
|
||||||
*/
|
|
||||||
getSelectedMode() {
|
|
||||||
return document.querySelector('input[name="darkmode"]:checked').value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the state of the radio buttons
|
|
||||||
* @param mode
|
|
||||||
*/
|
|
||||||
setButtonMode(mode) {
|
|
||||||
document.querySelector('input[name="darkmode"][value="' + mode + '"]').checked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable darkmode by adding the data-bs-theme="dark" to the html tag
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_enableDarkmode() {
|
|
||||||
//Add data-bs-theme="dark" to the html tag
|
|
||||||
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable darkmode by adding the data-bs-theme="light" to the html tag
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_disableDarkmode() {
|
|
||||||
//Set data-bs-theme to light
|
|
||||||
document.documentElement.setAttribute('data-bs-theme', 'light');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the darkmode to auto and enable/disable it depending on the system settings, also add
|
|
||||||
* an event listener to change the darkmode if the system settings change
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_setDarkmodeAuto() {
|
|
||||||
if (this.getMode() !== 'auto') {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
try {
|
||||||
this._enableDarkmode();
|
const darkmode = new Darkmode();
|
||||||
} else {
|
this._darkmode = darkmode;
|
||||||
this._disableDarkmode();
|
|
||||||
|
//Unhide darkmode button
|
||||||
|
this._showWidget();
|
||||||
|
|
||||||
|
//Set the switch according to our current darkmode state
|
||||||
|
const toggler = document.getElementById("toggleDarkmode");
|
||||||
|
toggler.checked = darkmode.isActivated();
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
|
||||||
console.log('Prefered color scheme changed to ' + event.matches ? 'dark' : 'light');
|
|
||||||
this._setDarkmodeAuto();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
_showWidget() {
|
||||||
* Check if darkmode is activated
|
this.element.classList.remove('hidden');
|
||||||
* @return {boolean}
|
}
|
||||||
*/
|
|
||||||
isDarkmodeActivated() {
|
toggleDarkmode() {
|
||||||
return document.documentElement.getAttribute('data-bs-theme') === 'dark';
|
this._darkmode.toggle();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -88,8 +88,5 @@ export default class extends Controller {
|
||||||
} else {
|
} else {
|
||||||
this.hideSidebar();
|
this.hideSidebar();
|
||||||
}
|
}
|
||||||
|
|
||||||
//Hide the tootip on the button
|
|
||||||
this._toggle_button.blur();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,26 +20,16 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { Controller } from '@hotwired/stimulus';
|
import { Controller } from '@hotwired/stimulus';
|
||||||
import { Marked } from "marked";
|
import { marked } from "marked";
|
||||||
import { mangle } from "marked-mangle";
|
|
||||||
import { gfmHeadingId } from "marked-gfm-heading-id";
|
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
import "../../css/app/markdown.css";
|
import "../../css/app/markdown.css";
|
||||||
|
|
||||||
export default class MarkdownController extends Controller {
|
export default class extends Controller {
|
||||||
|
|
||||||
static _marked = new Marked([
|
|
||||||
{
|
|
||||||
gfm: true,
|
|
||||||
},
|
|
||||||
gfmHeadingId(),
|
|
||||||
mangle(),
|
|
||||||
])
|
|
||||||
;
|
|
||||||
|
|
||||||
connect()
|
connect()
|
||||||
{
|
{
|
||||||
|
this.configureMarked();
|
||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
//Dispatch an event that we are now finished
|
//Dispatch an event that we are now finished
|
||||||
|
@ -53,7 +43,7 @@ export default class MarkdownController extends Controller {
|
||||||
let raw = this.element.dataset['markdown'];
|
let raw = this.element.dataset['markdown'];
|
||||||
|
|
||||||
//Apply purified parsed markdown
|
//Apply purified parsed markdown
|
||||||
this.element.innerHTML = DOMPurify.sanitize(MarkdownController._marked.parse(this.unescapeHTML(raw)));
|
this.element.innerHTML = DOMPurify.sanitize(marked(this.unescapeHTML(raw)));
|
||||||
|
|
||||||
for(let a of this.element.querySelectorAll('a')) {
|
for(let a of this.element.querySelectorAll('a')) {
|
||||||
//Mark all links as external
|
//Mark all links as external
|
||||||
|
@ -89,23 +79,10 @@ export default class MarkdownController extends Controller {
|
||||||
/**
|
/**
|
||||||
* Configure the marked parser
|
* Configure the marked parser
|
||||||
*/
|
*/
|
||||||
/*static newMarked()
|
configureMarked()
|
||||||
{
|
{
|
||||||
const marked = new Marked([
|
|
||||||
{
|
|
||||||
gfm: true,
|
|
||||||
},
|
|
||||||
gfmHeadingId(),
|
|
||||||
mangle(),
|
|
||||||
])
|
|
||||||
;
|
|
||||||
|
|
||||||
marked.use(mangle());
|
|
||||||
marked.use(gfmHeadingId({
|
|
||||||
}));
|
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
gfm: true,
|
gfm: true,
|
||||||
});
|
});
|
||||||
}*/
|
}
|
||||||
}
|
}
|
|
@ -23,12 +23,6 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
|
||||||
import '../../css/components/tom-select_extensions.css';
|
import '../../css/components/tom-select_extensions.css';
|
||||||
import TomSelect from "tom-select";
|
import TomSelect from "tom-select";
|
||||||
|
|
||||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
|
||||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
|
||||||
|
|
||||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
|
||||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
_tomSelect;
|
_tomSelect;
|
||||||
|
|
||||||
|
@ -52,12 +46,6 @@ export default class extends Controller {
|
||||||
}
|
}
|
||||||
return '<div>' + escape(data.label) + '</div>';
|
return '<div>' + escape(data.label) + '</div>';
|
||||||
}
|
}
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
'autoselect_typed': {},
|
|
||||||
'click_to_edit': {},
|
|
||||||
'clear_button': {},
|
|
||||||
"restore_on_backspace": {}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,6 @@ export default class extends Controller {
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
language: language,
|
language: language,
|
||||||
licenseKey: "GPL",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const watchdog = new EditorWatchdog();
|
const watchdog = new EditorWatchdog();
|
||||||
|
@ -71,9 +70,7 @@ export default class extends Controller {
|
||||||
editor_div.classList.add(...new_classes.split(","));
|
editor_div.classList.add(...new_classes.split(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
//This return is important! Otherwise we get mysterious errors in the console
|
console.log(editor);
|
||||||
//See: https://github.com/ckeditor/ckeditor5/issues/5897#issuecomment-628471302
|
|
||||||
return editor;
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default class extends Controller {
|
||||||
|
|
||||||
if(!prototype) {
|
if(!prototype) {
|
||||||
console.warn("Prototype is not set, we cannot create a new element. This is most likely due to missing permissions.");
|
console.warn("Prototype is not set, we cannot create a new element. This is most likely due to missing permissions.");
|
||||||
bootbox.alert("You do not have the permissions to create a new element. (No protoype element is set)");
|
bootbox.alert("You do not have the permsissions to create a new element. (No protoype element is set)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,49 +75,13 @@ export default class extends Controller {
|
||||||
|
|
||||||
//Insert new html after the last child element
|
//Insert new html after the last child element
|
||||||
//If the table has a tbody, insert it there
|
//If the table has a tbody, insert it there
|
||||||
//Afterwards return the newly created row
|
|
||||||
if(targetTable.tBodies[0]) {
|
if(targetTable.tBodies[0]) {
|
||||||
targetTable.tBodies[0].insertAdjacentHTML('beforeend', newElementStr);
|
targetTable.tBodies[0].insertAdjacentHTML('beforeend', newElementStr);
|
||||||
return targetTable.tBodies[0].lastElementChild;
|
|
||||||
} else { //Otherwise just insert it
|
} else { //Otherwise just insert it
|
||||||
targetTable.insertAdjacentHTML('beforeend', newElementStr);
|
targetTable.insertAdjacentHTML('beforeend', newElementStr);
|
||||||
return targetTable.lastElementChild;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This action opens a file dialog to select multiple files and then creates a new element for each file, where
|
|
||||||
* the file is assigned to the input field.
|
|
||||||
* This should only be used for attachments collection types
|
|
||||||
* @param event
|
|
||||||
*/
|
|
||||||
uploadMultipleFiles(event) {
|
|
||||||
//Open a file dialog to select multiple files
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.multiple = true;
|
|
||||||
input.click();
|
|
||||||
|
|
||||||
input.addEventListener('change', (event) => {
|
|
||||||
//Create a element for each file
|
|
||||||
|
|
||||||
for (let i = 0; i < input.files.length; i++) {
|
|
||||||
const file = input.files[i];
|
|
||||||
|
|
||||||
const newElement = this.createElement(event);
|
|
||||||
const rowInput = newElement.querySelector("input[type='file']");
|
|
||||||
|
|
||||||
//We can not directly assign the file to the input, so we have to create a new DataTransfer object
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
dataTransfer.items.add(file);
|
|
||||||
|
|
||||||
rowInput.files = dataTransfer.files;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Similar to createEvent Pricedetails need some special handling to fill min amount
|
* Similar to createEvent Pricedetails need some special handling to fill min amount
|
||||||
* @param event
|
* @param event
|
||||||
|
|
|
@ -24,25 +24,18 @@ import 'datatables.net-bs5/css/dataTables.bootstrap5.css'
|
||||||
import 'datatables.net-buttons-bs5/css/buttons.bootstrap5.css'
|
import 'datatables.net-buttons-bs5/css/buttons.bootstrap5.css'
|
||||||
import 'datatables.net-fixedheader-bs5/css/fixedHeader.bootstrap5.css'
|
import 'datatables.net-fixedheader-bs5/css/fixedHeader.bootstrap5.css'
|
||||||
import 'datatables.net-responsive-bs5/css/responsive.bootstrap5.css';
|
import 'datatables.net-responsive-bs5/css/responsive.bootstrap5.css';
|
||||||
|
import 'datatables.net-select-bs5/css/select.bootstrap5.css';
|
||||||
//Use our own styles for the select extension which fit the bootstrap theme better
|
|
||||||
//import 'datatables.net-select-bs5/css/select.bootstrap5.css';
|
|
||||||
import '../../../css/components/datatables_select_bs5.css';
|
|
||||||
|
|
||||||
//JS
|
//JS
|
||||||
import 'datatables.net-bs5';
|
import 'datatables.net-bs5';
|
||||||
import 'datatables.net-buttons-bs5';
|
import 'datatables.net-buttons-bs5';
|
||||||
import 'datatables.net-buttons/js/buttons.colVis.js';
|
import 'datatables.net-buttons/js/buttons.colVis.js';
|
||||||
import 'datatables.net-fixedheader-bs5';
|
import 'datatables.net-fixedheader-bs5';
|
||||||
|
import 'datatables.net-select-bs5';
|
||||||
import 'datatables.net-colreorder-bs5';
|
import 'datatables.net-colreorder-bs5';
|
||||||
import 'datatables.net-responsive-bs5';
|
import 'datatables.net-responsive-bs5';
|
||||||
import '../../../js/lib/datatables';
|
import '../../../js/lib/datatables';
|
||||||
|
|
||||||
//import 'datatables.net-select-bs5';
|
|
||||||
//Use the local version containing the fix for the select extension
|
|
||||||
import '../../../js/lib/dataTables.select.mjs';
|
|
||||||
|
|
||||||
|
|
||||||
const EVENT_DT_LOADED = 'dt:loaded';
|
const EVENT_DT_LOADED = 'dt:loaded';
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
@ -72,19 +65,12 @@ export default class extends Controller {
|
||||||
localStorage.setItem( this.getStateSaveKey(), JSON.stringify(data) );
|
localStorage.setItem( this.getStateSaveKey(), JSON.stringify(data) );
|
||||||
}
|
}
|
||||||
|
|
||||||
stateLoadCallback() {
|
stateLoadCallback(settings) {
|
||||||
const json = localStorage.getItem(this.getStateSaveKey());
|
const data = JSON.parse( localStorage.getItem(this.getStateSaveKey()) );
|
||||||
if(json === null || json === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = JSON.parse(json);
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
//Do not save the start value (current page), as we want to always start at the first page on a page reload
|
//Do not save the start value (current page), as we want to always start at the first page on a page reload
|
||||||
delete data.start;
|
data.start = 0;
|
||||||
//Reset the data length to the default value by deleting the length property
|
|
||||||
delete data.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
@ -102,19 +88,6 @@ export default class extends Controller {
|
||||||
//Add url info, as the one available in the history is not enough, as Turbo may have not changed it yet
|
//Add url info, as the one available in the history is not enough, as Turbo may have not changed it yet
|
||||||
settings.url = this.element.dataset.dtUrl;
|
settings.url = this.element.dataset.dtUrl;
|
||||||
|
|
||||||
//Add initial_order info to the settings, so that the order on the initial page load is the one saved in the state
|
|
||||||
const saved_state = this.stateLoadCallback();
|
|
||||||
if (saved_state !== null) {
|
|
||||||
const raw_order = saved_state.order;
|
|
||||||
|
|
||||||
settings.initial_order = raw_order.map((order) => {
|
|
||||||
return {
|
|
||||||
column: order[0],
|
|
||||||
dir: order[1]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
colReorder: true,
|
colReorder: true,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
|
@ -124,7 +97,7 @@ export default class extends Controller {
|
||||||
},
|
},
|
||||||
buttons: [{
|
buttons: [{
|
||||||
"extend": 'colvis',
|
"extend": 'colvis',
|
||||||
'className': 'mr-2 btn-outline-secondary',
|
'className': 'mr-2 btn-light',
|
||||||
'columns': ':not(.no-colvis)',
|
'columns': ':not(.no-colvis)',
|
||||||
"text": "<i class='fa fa-cog'></i>"
|
"text": "<i class='fa fa-cog'></i>"
|
||||||
}],
|
}],
|
||||||
|
@ -139,7 +112,7 @@ export default class extends Controller {
|
||||||
if(this.isSelectable()) {
|
if(this.isSelectable()) {
|
||||||
options.select = {
|
options.select = {
|
||||||
style: 'multi+shift',
|
style: 'multi+shift',
|
||||||
selector: 'td.dt-select',
|
selector: 'td.select-checkbox'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,28 +123,6 @@ export default class extends Controller {
|
||||||
console.error("Error initializing datatables: " + err);
|
console.error("Error initializing datatables: " + err);
|
||||||
});
|
});
|
||||||
|
|
||||||
//Fix height of the length selector
|
|
||||||
promise.then((dt) => {
|
|
||||||
|
|
||||||
//Draw the rows to make sure the correct status text is displayed ("No matching records found" instead of "Loading...")
|
|
||||||
if (dt.data().length === 0) {
|
|
||||||
dt.rows().draw()
|
|
||||||
}
|
|
||||||
|
|
||||||
//Find all length selectors (select with name dt_length), which are inside a label
|
|
||||||
const lengthSelectors = document.querySelectorAll('label select[name="dt_length"]');
|
|
||||||
//And remove the surrounding label, while keeping the select with all event handlers
|
|
||||||
lengthSelectors.forEach((selector) => {
|
|
||||||
selector.parentElement.replaceWith(selector);
|
|
||||||
});
|
|
||||||
|
|
||||||
//Find all column visibility buttons (button with buttons-colvis class) and remove the btn-secondary class
|
|
||||||
const colVisButtons = document.querySelectorAll('button.buttons-colvis');
|
|
||||||
colVisButtons.forEach((button) => {
|
|
||||||
button.classList.remove('btn-secondary');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//Dispatch an event to let others know that the datatables has been loaded
|
//Dispatch an event to let others know that the datatables has been loaded
|
||||||
promise.then((dt) => {
|
promise.then((dt) => {
|
||||||
const event = new CustomEvent(EVENT_DT_LOADED, {bubbles: true});
|
const event = new CustomEvent(EVENT_DT_LOADED, {bubbles: true});
|
||||||
|
@ -193,6 +144,27 @@ export default class extends Controller {
|
||||||
dt.fixedHeader.headerOffset($("#navbar").outerHeight());
|
dt.fixedHeader.headerOffset($("#navbar").outerHeight());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Register event handler to selectAllRows checkbox if available
|
||||||
|
if (this.isSelectable()) {
|
||||||
|
promise.then((dt) => {
|
||||||
|
const selectAllCheckbox = this.element.querySelector('thead th.select-checkbox');
|
||||||
|
selectAllCheckbox.addEventListener('click', () => {
|
||||||
|
if(selectAllCheckbox.parentElement.classList.contains('selected')) {
|
||||||
|
dt.rows().deselect();
|
||||||
|
selectAllCheckbox.parentElement.classList.remove('selected');
|
||||||
|
} else {
|
||||||
|
dt.rows().select();
|
||||||
|
selectAllCheckbox.parentElement.classList.add('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//When any row is deselected, also deselect the selectAll checkbox
|
||||||
|
dt.on('deselect.dt', () => {
|
||||||
|
selectAllCheckbox.parentElement.classList.remove('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//Allow to further configure the datatable
|
//Allow to further configure the datatable
|
||||||
promise.then(this._afterLoaded.bind(this));
|
promise.then(this._afterLoaded.bind(this));
|
||||||
|
|
||||||
|
@ -231,16 +203,4 @@ export default class extends Controller {
|
||||||
return this.element.dataset.select ?? false;
|
return this.element.dataset.select ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
invertSelection() {
|
|
||||||
//Do nothing if the datatable is not selectable
|
|
||||||
if(!this.isSelectable()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Invert the selected rows on the datatable
|
|
||||||
const selected_rows = this._dt.rows({selected: true});
|
|
||||||
this._dt.rows().select();
|
|
||||||
selected_rows.deselect();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,9 +43,7 @@ export default class extends Controller
|
||||||
const message = this.element.dataset.deleteMessage;
|
const message = this.element.dataset.deleteMessage;
|
||||||
const title = this.element.dataset.deleteTitle;
|
const title = this.element.dataset.deleteTitle;
|
||||||
|
|
||||||
//Use event target, to find the form, where the submit button was clicked
|
const form = this.element;
|
||||||
const form = event.target;
|
|
||||||
const submitter = event.submitter;
|
|
||||||
const that = this;
|
const that = this;
|
||||||
|
|
||||||
const confirm = bootbox.confirm({
|
const confirm = bootbox.confirm({
|
||||||
|
@ -60,14 +58,6 @@ export default class extends Controller
|
||||||
const submit_btn = document.createElement('button');
|
const submit_btn = document.createElement('button');
|
||||||
submit_btn.type = 'submit';
|
submit_btn.type = 'submit';
|
||||||
submit_btn.style.display = 'none';
|
submit_btn.style.display = 'none';
|
||||||
|
|
||||||
//If the clicked button has a value, set it on the submit button
|
|
||||||
if (submitter.value) {
|
|
||||||
submit_btn.value = submitter.value;
|
|
||||||
}
|
|
||||||
if (submitter.name) {
|
|
||||||
submit_btn.name = submitter.name;
|
|
||||||
}
|
|
||||||
form.appendChild(submit_btn);
|
form.appendChild(submit_btn);
|
||||||
submit_btn.click();
|
submit_btn.click();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
|
||||||
|
|
||||||
import JSONFormatter from 'json-formatter-js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This controller implements an element that renders a JSON object as a collapsible tree.
|
|
||||||
* The JSON object is passed as a data attribute.
|
|
||||||
* You have to apply the controller to a div element or similar block element which can contain other elements.
|
|
||||||
*/
|
|
||||||
export default class extends Controller {
|
|
||||||
connect() {
|
|
||||||
const depth_to_open = this.element.dataset.depthToOpen ?? 0;
|
|
||||||
const json_string = this.element.dataset.json;
|
|
||||||
const json_object = JSON.parse(json_string);
|
|
||||||
|
|
||||||
const formatter = new JSONFormatter(json_object, depth_to_open);
|
|
||||||
|
|
||||||
this.element.appendChild(formatter.render());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
|
||||||
|
|
||||||
import * as bootbox from "bootbox";
|
|
||||||
import "../../css/components/bootbox_extensions.css";
|
|
||||||
|
|
||||||
export default class extends Controller
|
|
||||||
{
|
|
||||||
|
|
||||||
static values = {
|
|
||||||
message: String,
|
|
||||||
title: String
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
connect()
|
|
||||||
{
|
|
||||||
this._confirmed = false;
|
|
||||||
|
|
||||||
this.element.addEventListener('click', this._onClick.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
_onClick(event)
|
|
||||||
{
|
|
||||||
|
|
||||||
//If a user has not already confirmed the deletion, just let turbo do its work
|
|
||||||
if (this._confirmed) {
|
|
||||||
this._confirmed = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const that = this;
|
|
||||||
|
|
||||||
bootbox.confirm({
|
|
||||||
title: this.titleValue,
|
|
||||||
message: this.messageValue,
|
|
||||||
callback: (result) => {
|
|
||||||
if (result) {
|
|
||||||
//Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form
|
|
||||||
that._confirmed = true;
|
|
||||||
|
|
||||||
//Click the link
|
|
||||||
that.element.click();
|
|
||||||
} else {
|
|
||||||
that._confirmed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
|
||||||
|
|
||||||
export default class extends Controller
|
|
||||||
{
|
|
||||||
static values = {
|
|
||||||
id: String
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
this.loadState()
|
|
||||||
this.element.addEventListener('change', () => {
|
|
||||||
this.saveState()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadState() {
|
|
||||||
let storageKey = this.getStorageKey();
|
|
||||||
let value = localStorage.getItem(storageKey);
|
|
||||||
if (value === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === 'true') {
|
|
||||||
this.element.checked = true
|
|
||||||
}
|
|
||||||
if (value === 'false') {
|
|
||||||
this.element.checked = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveState() {
|
|
||||||
let storageKey = this.getStorageKey();
|
|
||||||
|
|
||||||
if (this.element.checked) {
|
|
||||||
localStorage.setItem(storageKey, 'true');
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(storageKey, 'false');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getStorageKey() {
|
|
||||||
if (this.hasIdValue) {
|
|
||||||
return 'persistent_checkbox_' + this.idValue
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'persistent_checkbox_' + this.element.id;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,200 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Controller } from "@hotwired/stimulus";
|
|
||||||
import { autocomplete } from '@algolia/autocomplete-js';
|
|
||||||
//import "@algolia/autocomplete-theme-classic/dist/theme.css";
|
|
||||||
import "../../css/components/autocomplete_bootstrap_theme.css";
|
|
||||||
import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches';
|
|
||||||
import {marked} from "marked";
|
|
||||||
|
|
||||||
import {
|
|
||||||
trans,
|
|
||||||
SEARCH_PLACEHOLDER,
|
|
||||||
SEARCH_SUBMIT,
|
|
||||||
STATISTICS_PARTS
|
|
||||||
} from '../../translator';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This controller is responsible for the search fields in the navbar and the homepage.
|
|
||||||
* It uses the Algolia Autocomplete library to provide a fast and responsive search.
|
|
||||||
*/
|
|
||||||
export default class extends Controller {
|
|
||||||
|
|
||||||
static targets = ["input"];
|
|
||||||
|
|
||||||
_autocomplete;
|
|
||||||
|
|
||||||
// Highlight the search query in the results
|
|
||||||
_highlight = (text, query) => {
|
|
||||||
if (!text) return text;
|
|
||||||
if (!query) return text;
|
|
||||||
|
|
||||||
const HIGHLIGHT_PRE_TAG = '__aa-highlight__'
|
|
||||||
const HIGHLIGHT_POST_TAG = '__/aa-highlight__'
|
|
||||||
|
|
||||||
const escaped = query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
||||||
const regex = new RegExp(escaped, 'gi');
|
|
||||||
|
|
||||||
return text.replace(regex, (match) => `${HIGHLIGHT_PRE_TAG}${match}${HIGHLIGHT_POST_TAG}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
// The endpoint for searching parts
|
|
||||||
const base_url = this.element.dataset.autocomplete;
|
|
||||||
// The URL template for the part detail pages
|
|
||||||
const part_detail_uri_template = this.element.dataset.detailUrl;
|
|
||||||
|
|
||||||
//The URL of the placeholder picture
|
|
||||||
const placeholder_image = this.element.dataset.placeholderImage;
|
|
||||||
|
|
||||||
//If the element is in navbar mode, or not
|
|
||||||
const navbar_mode = this.element.dataset.navbarMode === "true";
|
|
||||||
|
|
||||||
const that = this;
|
|
||||||
|
|
||||||
const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
|
|
||||||
key: 'RECENT_SEARCH',
|
|
||||||
limit: 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
this._autocomplete = autocomplete({
|
|
||||||
container: this.element,
|
|
||||||
//Place the panel in the navbar, if the element is in navbar mode
|
|
||||||
panelContainer: navbar_mode ? document.getElementById("navbar-search-form") : document.body,
|
|
||||||
panelPlacement: this.element.dataset.panelPlacement,
|
|
||||||
plugins: [recentSearchesPlugin],
|
|
||||||
openOnFocus: true,
|
|
||||||
placeholder: trans(SEARCH_PLACEHOLDER),
|
|
||||||
translations: {
|
|
||||||
submitButtonTitle: trans(SEARCH_SUBMIT)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Use a navigator compatible with turbo:
|
|
||||||
navigator: {
|
|
||||||
navigate({ itemUrl }) {
|
|
||||||
window.Turbo.visit(itemUrl, { action: "advance" });
|
|
||||||
},
|
|
||||||
navigateNewTab({ itemUrl }) {
|
|
||||||
const windowReference = window.open(itemUrl, '_blank', 'noopener');
|
|
||||||
|
|
||||||
if (windowReference) {
|
|
||||||
windowReference.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigateNewWindow({ itemUrl }) {
|
|
||||||
window.open(itemUrl, '_blank', 'noopener');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// If the form is submitted, forward the term to the form
|
|
||||||
onSubmit({state, event, ...setters}) {
|
|
||||||
//Put the current text into each target input field
|
|
||||||
const input = that.inputTarget;
|
|
||||||
|
|
||||||
if (!input) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Do not submit the form, if the input is empty
|
|
||||||
if (state.query === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.value = state.query;
|
|
||||||
input.form.requestSubmit();
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
getSources({ query }) {
|
|
||||||
return [
|
|
||||||
// The parts source
|
|
||||||
{
|
|
||||||
sourceId: 'parts',
|
|
||||||
getItems() {
|
|
||||||
const url = base_url.replace('__QUERY__', encodeURIComponent(query));
|
|
||||||
|
|
||||||
const data = fetch(url)
|
|
||||||
.then((response) => response.json())
|
|
||||||
;
|
|
||||||
|
|
||||||
//Iterate over all fields besides the id and highlight them
|
|
||||||
const fields = ["name", "description", "category", "footprint"];
|
|
||||||
|
|
||||||
data.then((items) => {
|
|
||||||
items.forEach((item) => {
|
|
||||||
for (const field of fields) {
|
|
||||||
item[field] = that._highlight(item[field], query);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
getItemUrl({ item }) {
|
|
||||||
return part_detail_uri_template.replace('__ID__', item.id);
|
|
||||||
},
|
|
||||||
templates: {
|
|
||||||
header({ html }) {
|
|
||||||
return html`<span class="aa-SourceHeaderTitle">${trans(STATISTICS_PARTS)}</span>
|
|
||||||
<div class="aa-SourceHeaderLine" />`;
|
|
||||||
},
|
|
||||||
item({item, components, html}) {
|
|
||||||
const details_url = part_detail_uri_template.replace('__ID__', item.id);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<a class="aa-ItemLink" href="${details_url}">
|
|
||||||
<div class="aa-ItemContent">
|
|
||||||
<div class="aa-ItemIcon aa-ItemIcon--picture aa-ItemIcon--alignTop">
|
|
||||||
<img src="${item.image !== "" ? item.image : placeholder_image}" alt="${item.name}" width="30" height="30"/>
|
|
||||||
</div>
|
|
||||||
<div class="aa-ItemContentBody">
|
|
||||||
<div class="aa-ItemContentTitle">
|
|
||||||
<b>
|
|
||||||
${components.Highlight({hit: item, attribute: 'name'})}
|
|
||||||
</b>
|
|
||||||
</div>
|
|
||||||
<div class="aa-ItemContentDescription">
|
|
||||||
${components.Highlight({hit: item, attribute: 'description'})}
|
|
||||||
${item.category ? html`<p class="m-0"><span class="fa-solid fa-tags fa-fw"></span>${components.Highlight({hit: item, attribute: 'category'})}</p>` : ""}
|
|
||||||
${item.footprint ? html`<p class="m-0"><span class="fa-solid fa-microchip fa-fw"></span>${components.Highlight({hit: item, attribute: 'footprint'})}</p>` : ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//Try to find the input field and register a defocus handler. This is necessarry, as by default the autocomplete
|
|
||||||
//lib has problems when multiple inputs are present on the page. (see https://github.com/algolia/autocomplete/issues/1216)
|
|
||||||
const inputs = this.element.getElementsByClassName('aa-Input');
|
|
||||||
for (const input of inputs) {
|
|
||||||
input.addEventListener('blur', () => {
|
|
||||||
this._autocomplete.setIsOpen(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,7 +27,7 @@ export default class extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
let tmp = '<div class="row m-0">' +
|
let tmp = '<div class="row m-0">' +
|
||||||
"<div class='col-2 p-0 d-flex align-items-center' style='max-width: 80px;'>" +
|
"<div class='col-2 p-0 d-flex align-items-center'>" +
|
||||||
(data.image ? "<img class='typeahead-image' src='" + data.image + "'/>" : "") +
|
(data.image ? "<img class='typeahead-image' src='" + data.image + "'/>" : "") +
|
||||||
"</div>" +
|
"</div>" +
|
||||||
"<div class='col-10'>" +
|
"<div class='col-10'>" +
|
||||||
|
|
|
@ -1,123 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
|
||||||
import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
|
|
||||||
import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
|
|
||||||
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
|
|
||||||
import * as zxcvbnDePackage from '@zxcvbn-ts/language-de';
|
|
||||||
import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr';
|
|
||||||
import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja';
|
|
||||||
import {trans, USER_PASSWORD_STRENGTH_VERY_WEAK, USER_PASSWORD_STRENGTH_WEAK, USER_PASSWORD_STRENGTH_MEDIUM,
|
|
||||||
USER_PASSWORD_STRENGTH_STRONG, USER_PASSWORD_STRENGTH_VERY_STRONG} from '../../translator.js';
|
|
||||||
|
|
||||||
/* stimulusFetch: 'lazy' */
|
|
||||||
export default class extends Controller {
|
|
||||||
|
|
||||||
_passwordInput;
|
|
||||||
|
|
||||||
static targets = ["badge", "warning"]
|
|
||||||
|
|
||||||
_getTranslations() {
|
|
||||||
//Get the current locale
|
|
||||||
const locale = document.documentElement.lang;
|
|
||||||
if (locale.includes('de')) {
|
|
||||||
return zxcvbnDePackage.translations;
|
|
||||||
} else if (locale.includes('fr')) {
|
|
||||||
return zxcvbnFrPackage.translations;
|
|
||||||
} else if (locale.includes('ja')) {
|
|
||||||
return zxcvbnJaPackage.translations;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Fallback to english
|
|
||||||
return zxcvbnEnPackage.translations;
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
//Find the password input field
|
|
||||||
this._passwordInput = this.element.querySelector('input[type="password"]');
|
|
||||||
|
|
||||||
//Configure zxcvbn
|
|
||||||
const options = {
|
|
||||||
graphs: zxcvbnCommonPackage.adjacencyGraphs,
|
|
||||||
dictionary: {
|
|
||||||
...zxcvbnCommonPackage.dictionary,
|
|
||||||
// We could use the english dictionary here too, but it is very big. So we just use the common words
|
|
||||||
//...zxcvbnEnPackage.dictionary,
|
|
||||||
},
|
|
||||||
translations: this._getTranslations(),
|
|
||||||
};
|
|
||||||
zxcvbnOptions.setOptions(options);
|
|
||||||
|
|
||||||
//Add event listener to the password input field
|
|
||||||
this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
_onPasswordInput() {
|
|
||||||
//Retrieve the password
|
|
||||||
const password = this._passwordInput.value;
|
|
||||||
|
|
||||||
//Estimate the password strength
|
|
||||||
const result = zxcvbn(password);
|
|
||||||
|
|
||||||
//Update the badge
|
|
||||||
this.badgeTarget.parentElement.classList.remove("d-none");
|
|
||||||
this._setBadgeToLevel(result.score);
|
|
||||||
|
|
||||||
this.warningTarget.innerHTML = result.feedback.warning;
|
|
||||||
}
|
|
||||||
|
|
||||||
_setBadgeToLevel(level) {
|
|
||||||
let text, classes;
|
|
||||||
|
|
||||||
switch (level) {
|
|
||||||
case 0:
|
|
||||||
text = trans(USER_PASSWORD_STRENGTH_VERY_WEAK);
|
|
||||||
classes = "bg-danger badge-danger";
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
text = trans(USER_PASSWORD_STRENGTH_WEAK);
|
|
||||||
classes = "bg-warning badge-warning";
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
text = trans(USER_PASSWORD_STRENGTH_MEDIUM)
|
|
||||||
classes = "bg-info badge-info";
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
text = trans(USER_PASSWORD_STRENGTH_STRONG);
|
|
||||||
classes = "bg-primary badge-primary";
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
text = trans(USER_PASSWORD_STRENGTH_VERY_STRONG);
|
|
||||||
classes = "bg-success badge-success";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
text = "???";
|
|
||||||
classes = "bg-secondary badge-secondary";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.badgeTarget.innerHTML = text;
|
|
||||||
//Remove all classes
|
|
||||||
this.badgeTarget.className = '';
|
|
||||||
//Re-add the classes
|
|
||||||
this.badgeTarget.classList.add("badge");
|
|
||||||
this.badgeTarget.classList.add(...classes.split(" "));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
|
||||||
|
|
||||||
import "tom-select/dist/css/tom-select.bootstrap5.css";
|
|
||||||
import '../../css/components/tom-select_extensions.css';
|
|
||||||
import TomSelect from "tom-select";
|
|
||||||
|
|
||||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
|
||||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
|
||||||
|
|
||||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
|
||||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the frontend controller for StaticFileAutocompleteType form element.
|
|
||||||
* Basically it loads a text file from the given url (via data-url) and uses it as a source for the autocomplete.
|
|
||||||
* The file is just a list of strings, one per line, which will be used as the autocomplete options.
|
|
||||||
* Lines starting with # will be ignored.
|
|
||||||
*/
|
|
||||||
export default class extends Controller {
|
|
||||||
_tomSelect;
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
|
|
||||||
let settings = {
|
|
||||||
persistent: false,
|
|
||||||
create: true,
|
|
||||||
maxItems: 1,
|
|
||||||
maxOptions: 100,
|
|
||||||
createOnBlur: true,
|
|
||||||
selectOnTab: true,
|
|
||||||
valueField: 'text',
|
|
||||||
searchField: 'text',
|
|
||||||
orderField: 'text',
|
|
||||||
|
|
||||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
|
||||||
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
|
||||||
plugins: {
|
|
||||||
'autoselect_typed': {},
|
|
||||||
'click_to_edit': {},
|
|
||||||
'clear_button': {},
|
|
||||||
'restore_on_backspace': {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.element.dataset.url) {
|
|
||||||
const url = this.element.dataset.url;
|
|
||||||
settings.load = (query, callback) => {
|
|
||||||
const self = this;
|
|
||||||
if (self.loading > 1) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(url)
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(text => {
|
|
||||||
// Convert the text file to array
|
|
||||||
let lines = text.split("\n");
|
|
||||||
//Remove all lines beginning with #
|
|
||||||
lines = lines.filter(x => !x.startsWith("#"));
|
|
||||||
|
|
||||||
//Convert the array to an object, where each line is in the text field
|
|
||||||
lines = lines.map(x => {
|
|
||||||
return {text: x};
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
//Unset the load function to prevent endless recursion
|
|
||||||
self._tomSelect.settings.load = null;
|
|
||||||
|
|
||||||
callback(lines);
|
|
||||||
}).catch(() => {
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this._tomSelect = new TomSelect(this.element, settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
super.disconnect();
|
|
||||||
//Destroy the TomSelect instance
|
|
||||||
this._tomSelect.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -22,10 +22,6 @@ import '../../css/components/tom-select_extensions.css';
|
||||||
import TomSelect from "tom-select";
|
import TomSelect from "tom-select";
|
||||||
import {Controller} from "@hotwired/stimulus";
|
import {Controller} from "@hotwired/stimulus";
|
||||||
|
|
||||||
import {trans, ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB} from '../../translator.js'
|
|
||||||
|
|
||||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
|
||||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
_tomSelect;
|
_tomSelect;
|
||||||
|
@ -40,20 +36,12 @@ export default class extends Controller {
|
||||||
const allowAdd = this.element.getAttribute("data-allow-add") === "true";
|
const allowAdd = this.element.getAttribute("data-allow-add") === "true";
|
||||||
const addHint = this.element.getAttribute("data-add-hint") ?? "";
|
const addHint = this.element.getAttribute("data-add-hint") ?? "";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let settings = {
|
let settings = {
|
||||||
allowEmptyOption: true,
|
allowEmptyOption: true,
|
||||||
selectOnTab: true,
|
selectOnTab: true,
|
||||||
maxOptions: null,
|
maxOptions: null,
|
||||||
create: allowAdd ? this.createItem.bind(this) : false,
|
create: allowAdd,
|
||||||
createFilter: this.createFilter.bind(this),
|
createFilter: /\D/, //Must contain a non-digit character, otherwise they would be recognized as DB ID
|
||||||
|
|
||||||
// This three options allow us to paste element names with commas: (see issue #538)
|
|
||||||
maxItems: 1,
|
|
||||||
delimiter: "$$VERY_LONG_DELIMITER_THAT_SHOULD_NEVER_APPEAR$$",
|
|
||||||
splitOn: null,
|
|
||||||
|
|
||||||
searchField: [
|
searchField: [
|
||||||
{field: "text", weight : 2},
|
{field: "text", weight : 2},
|
||||||
|
@ -64,108 +52,15 @@ export default class extends Controller {
|
||||||
render: {
|
render: {
|
||||||
item: this.renderItem.bind(this),
|
item: this.renderItem.bind(this),
|
||||||
option: this.renderOption.bind(this),
|
option: this.renderOption.bind(this),
|
||||||
option_create: (data, escape) => {
|
option_create: function(data, escape) {
|
||||||
//If the input starts with "->", we prepend the current selected value, for easier extension of existing values
|
|
||||||
//This here handles the display part, while the createItem function handles the actual creation
|
|
||||||
if (data.input.startsWith("->")) {
|
|
||||||
//Get current selected value
|
|
||||||
const current = this._tomSelect.getItem(this._tomSelect.getValue()).textContent.replaceAll("→", "->").trim();
|
|
||||||
//Prepend it to the input
|
|
||||||
if (current) {
|
|
||||||
data.input = current + " " + data.input;
|
|
||||||
} else {
|
|
||||||
//If there is no current value, we remove the "->"
|
|
||||||
data.input = data.input.substring(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '<div class="create"><i class="fa-solid fa-plus fa-fw"></i> <strong>' + escape(data.input) + '</strong>… ' +
|
return '<div class="create"><i class="fa-solid fa-plus fa-fw"></i> <strong>' + escape(data.input) + '</strong>… ' +
|
||||||
'<small class="text-muted float-end">(' + addHint +')</small>' +
|
'<small class="text-muted float-end">(' + addHint +')</small>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
//Add callbacks to update validity
|
|
||||||
onInitialize: this.updateValidity.bind(this),
|
|
||||||
onChange: this.updateValidity.bind(this),
|
|
||||||
|
|
||||||
plugins: {
|
|
||||||
"autoselect_typed": {},
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//Add clear button plugin, if an empty option is present
|
|
||||||
if (this.element.querySelector("option[value='']") !== null) {
|
|
||||||
settings.plugins["clear_button"] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this._tomSelect = new TomSelect(this.element, settings);
|
this._tomSelect = new TomSelect(this.element, settings);
|
||||||
//Do not do a sync here as this breaks the initial rendering of the empty option
|
|
||||||
//this._tomSelect.sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
createItem(input, callback) {
|
|
||||||
|
|
||||||
//If the input starts with "->", we prepend the current selected value, for easier extension of existing values
|
|
||||||
if (input.startsWith("->")) {
|
|
||||||
//Get current selected value
|
|
||||||
let current = this._tomSelect.getItem(this._tomSelect.getValue()).textContent.replaceAll("→", "->").trim();
|
|
||||||
//Replace no break spaces with normal spaces
|
|
||||||
current = current.replaceAll("\u00A0", " ");
|
|
||||||
//Prepend it to the input
|
|
||||||
if (current) {
|
|
||||||
input = current + " " + input;
|
|
||||||
} else {
|
|
||||||
//If there is no current value, we remove the "->"
|
|
||||||
input = input.substring(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback({
|
|
||||||
//$%$ is a special value prefix, that is used to identify items, that are not yet in the DB
|
|
||||||
value: '$%$' + input,
|
|
||||||
text: input,
|
|
||||||
not_in_db_yet: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createFilter(input) {
|
|
||||||
|
|
||||||
//Normalize the input (replace spacing around arrows)
|
|
||||||
if (input.includes("->")) {
|
|
||||||
const inputs = input.split("->");
|
|
||||||
inputs.forEach((value, index) => {
|
|
||||||
inputs[index] = value.trim();
|
|
||||||
});
|
|
||||||
input = inputs.join("->");
|
|
||||||
} else {
|
|
||||||
input = input.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = this._tomSelect.options;
|
|
||||||
//Iterate over all options and check if the input is already present
|
|
||||||
for (let index in options) {
|
|
||||||
const option = options[index];
|
|
||||||
if (option.path === input) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
updateValidity() {
|
|
||||||
//Mark this input as invalid, if the selected option is disabled
|
|
||||||
|
|
||||||
const input = this.element;
|
|
||||||
const selectedOption = input.options[input.selectedIndex];
|
|
||||||
|
|
||||||
if (selectedOption && selectedOption.disabled) {
|
|
||||||
input.setCustomValidity("This option was disabled. Please select another option.");
|
|
||||||
} else {
|
|
||||||
input.setCustomValidity("");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTomSelect() {
|
getTomSelect() {
|
||||||
|
@ -183,27 +78,14 @@ export default class extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.short) {
|
if (data.short) {
|
||||||
let short = escape(data.short)
|
return '<div><b>' + escape(data.short) + '</b></div>';
|
||||||
|
|
||||||
//Make text italic, if the item is not yet in the DB
|
|
||||||
if (data.not_in_db_yet) {
|
|
||||||
short = '<i>' + short + '</i>';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '<div><b>' + short + '</b></div>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = "";
|
let name = "";
|
||||||
if (data.parent) {
|
if (data.parent) {
|
||||||
name += escape(data.parent) + " → ";
|
name += escape(data.parent) + " → ";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.not_in_db_yet) {
|
|
||||||
//Not yet added items are shown italic and with a badge
|
|
||||||
name += "<i><b>" + escape(data.text) + "</b></i>" + "<span class='ms-3 badge bg-info badge-info'>" + trans(ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB) + "</span>";
|
|
||||||
} else {
|
|
||||||
name += "<b>" + escape(data.text) + "</b>";
|
name += "<b>" + escape(data.text) + "</b>";
|
||||||
}
|
|
||||||
|
|
||||||
return '<div>' + (data.image ? "<img class='structural-entity-select-image' style='margin-right: 5px;' ' src='" + data.image + "'/>" : "") + name + '</div>';
|
return '<div>' + (data.image ? "<img class='structural-entity-select-image' style='margin-right: 5px;' ' src='" + data.image + "'/>" : "") + name + '</div>';
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,21 +23,14 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
|
||||||
import '../../css/components/tom-select_extensions.css';
|
import '../../css/components/tom-select_extensions.css';
|
||||||
import TomSelect from "tom-select";
|
import TomSelect from "tom-select";
|
||||||
|
|
||||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
|
||||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
|
||||||
|
|
||||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
|
||||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
_tomSelect;
|
_tomSelect;
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
let settings = {
|
let settings = {
|
||||||
plugins: {
|
plugins: {
|
||||||
remove_button:{},
|
remove_button:{
|
||||||
'autoselect_typed': {},
|
}
|
||||||
'click_to_edit': {},
|
|
||||||
},
|
},
|
||||||
persistent: false,
|
persistent: false,
|
||||||
selectOnTab: true,
|
selectOnTab: true,
|
||||||
|
|
|
@ -81,71 +81,31 @@ export default class extends Controller {
|
||||||
this._tree.remove();
|
this._tree.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
const BS53Theme = {
|
|
||||||
getOptions() {
|
|
||||||
return {
|
|
||||||
onhoverColor: 'var(--bs-secondary-bg)',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._tree = new BSTreeView(this.treeTarget, {
|
this._tree = new BSTreeView(this.treeTarget, {
|
||||||
levels: 1,
|
levels: 1,
|
||||||
showTags: this._showTags,
|
showTags: this._showTags,
|
||||||
data: data,
|
data: data,
|
||||||
showIcon: true,
|
showIcon: true,
|
||||||
preventUnselect: true,
|
|
||||||
allowReselect: true,
|
|
||||||
onNodeSelected: (event) => {
|
onNodeSelected: (event) => {
|
||||||
const node = event.detail.node;
|
const node = event.detail.node;
|
||||||
if (node.href) {
|
if (node.href) {
|
||||||
window.Turbo.visit(node.href, {action: "advance"});
|
window.Turbo.visit(node.href, {action: "advance"});
|
||||||
this._registerURLWatcher(node);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}, [BS5Theme, BS53Theme, FAIconTheme]);
|
//onNodeContextmenu: contextmenu_handler,
|
||||||
|
}, [BS5Theme, FAIconTheme]);
|
||||||
|
|
||||||
this.treeTarget.addEventListener(EVENT_INITIALIZED, (event) => {
|
this.treeTarget.addEventListener(EVENT_INITIALIZED, (event) => {
|
||||||
/** @type {BSTreeView} */
|
/** @type {BSTreeView} */
|
||||||
const treeView = event.detail.treeView;
|
const treeView = event.detail.treeView;
|
||||||
treeView.revealNode(treeView.getSelected());
|
treeView.revealNode(treeView.getSelected());
|
||||||
|
|
||||||
//Add the url watcher to all selected nodes
|
|
||||||
for (const node of treeView.getSelected()) {
|
|
||||||
this._registerURLWatcher(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Add contextmenu event listener to the tree, which allows us to open the links in a new tab with a right click
|
//Add contextmenu event listener to the tree, which allows us to open the links in a new tab with a right click
|
||||||
treeView.getTreeElement().addEventListener("contextmenu", this._onContextMenu.bind(this));
|
treeView.getTreeElement().addEventListener("contextmenu", this._onContextMenu.bind(this));
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_registerURLWatcher(node)
|
|
||||||
{
|
|
||||||
//Register a watcher for a location change, which will unselect the node, if the location changes
|
|
||||||
const desired_url = node.href;
|
|
||||||
|
|
||||||
//Ensure that the node is unselected, if the location changes
|
|
||||||
const unselectNode = () => {
|
|
||||||
//Parse url so we can properly compare them
|
|
||||||
const desired = new URL(node.href, window.location.origin);
|
|
||||||
|
|
||||||
//We only compare the pathname, because the hash and parameters should not matter
|
|
||||||
if(window.location.pathname !== desired.pathname) {
|
|
||||||
//The ignore parameter is important here, otherwise the node will not be unselected
|
|
||||||
node.setSelected(false, {silent: true, ignorePreventUnselect: true});
|
|
||||||
|
|
||||||
//Unregister the watcher
|
|
||||||
document.removeEventListener('turbo:load', unselectNode);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//Register the watcher via hotwire turbo
|
|
||||||
//We must just load to have the new url in window.location
|
|
||||||
document.addEventListener('turbo:load', unselectNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onContextMenu(event)
|
_onContextMenu(event)
|
||||||
{
|
{
|
||||||
//Find the node that was clicked and open link in new tab
|
//Find the node that was clicked and open link in new tab
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
|
|
||||||
static targets = [ "display", "select" ]
|
|
||||||
|
|
||||||
connect()
|
|
||||||
{
|
|
||||||
this.update();
|
|
||||||
this.selectTarget.addEventListener('change', this.update.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
update()
|
|
||||||
{
|
|
||||||
//If the select value is 0, then we show the input field
|
|
||||||
if( this.selectTarget.value === '0')
|
|
||||||
{
|
|
||||||
this.displayTarget.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.displayTarget.classList.add('d-none');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,7 +20,7 @@
|
||||||
import {Controller} from "@hotwired/stimulus";
|
import {Controller} from "@hotwired/stimulus";
|
||||||
//import * as ZXing from "@zxing/library";
|
//import * as ZXing from "@zxing/library";
|
||||||
|
|
||||||
import {Html5QrcodeScanner, Html5Qrcode} from "@part-db/html5-qrcode";
|
import {Html5QrcodeScanner, Html5Qrcode} from "html5-qrcode";
|
||||||
|
|
||||||
/* stimulusFetch: 'lazy' */
|
/* stimulusFetch: 'lazy' */
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
@ -50,7 +50,7 @@ export default class extends Controller {
|
||||||
});
|
});
|
||||||
|
|
||||||
this._scanner = new Html5QrcodeScanner(this.element.id, {
|
this._scanner = new Html5QrcodeScanner(this.element.id, {
|
||||||
fps: 10,
|
fps: 2,
|
||||||
qrbox: qrboxFunction,
|
qrbox: qrboxFunction,
|
||||||
experimentalFeatures: {
|
experimentalFeatures: {
|
||||||
//This option improves reading quality on android chrome
|
//This option improves reading quality on android chrome
|
||||||
|
@ -61,11 +61,6 @@ export default class extends Controller {
|
||||||
this._scanner.render(this.onScanSuccess.bind(this));
|
this._scanner.render(this.onScanSuccess.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
this._scanner.pause();
|
|
||||||
this._scanner.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
onScanSuccess(decodedText, decodedResult) {
|
onScanSuccess(decodedText, decodedResult) {
|
||||||
//Put our decoded Text into the input box
|
//Put our decoded Text into the input box
|
||||||
document.getElementById('scan_dialog_input').value = decodedText;
|
document.getElementById('scan_dialog_input').value = decodedText;
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This controller is used on a checkbox, which toggles the max value of all number input fields
|
|
||||||
*/
|
|
||||||
export default class extends Controller {
|
|
||||||
|
|
||||||
_checkbox;
|
|
||||||
|
|
||||||
getCheckbox() {
|
|
||||||
if (this._checkbox) {
|
|
||||||
return this._checkbox;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Find the checkbox inside the controller element
|
|
||||||
this._checkbox = this.element.querySelector('input[type="checkbox"]');
|
|
||||||
return this._checkbox;
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
//Add event listener to the checkbox
|
|
||||||
this.getCheckbox().addEventListener('change', this.toggleInputLimits.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleInputLimits() {
|
|
||||||
//Find all input fields with the data-toggle-input-limits-target="max"
|
|
||||||
const inputFields = document.querySelectorAll("input[type='number']");
|
|
||||||
|
|
||||||
inputFields.forEach((inputField) => {
|
|
||||||
//Ensure that the input field has either a max or a data-max attribute
|
|
||||||
if (!inputField.hasAttribute('max') && !inputField.hasAttribute('data-max')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//If the checkbox is checked, rename the max attribute to data-max
|
|
||||||
if (this.getCheckbox().checked) {
|
|
||||||
inputField.setAttribute('data-max', inputField.getAttribute('max'));
|
|
||||||
inputField.removeAttribute('max');
|
|
||||||
} else {
|
|
||||||
//If the checkbox is not checked, rename the data-max attribute back to max
|
|
||||||
inputField.setAttribute('max', inputField.getAttribute('data-max'));
|
|
||||||
inputField.removeAttribute('data-max');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,20 +25,9 @@ import "katex/dist/katex.css";
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ["input", "preview"];
|
static targets = ["input", "preview"];
|
||||||
|
|
||||||
static values = {
|
|
||||||
unit: {type: Boolean, default: false} //Render as upstanding (non-italic) text, useful for units
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePreview()
|
updatePreview()
|
||||||
{
|
{
|
||||||
let value = "";
|
katex.render(this.inputTarget.value, this.previewTarget, {
|
||||||
if (this.unitValue) {
|
|
||||||
value = "\\mathrm{" + this.inputTarget.value + "}";
|
|
||||||
} else {
|
|
||||||
value = this.inputTarget.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
katex.render(value, this.previewTarget, {
|
|
||||||
throwOnError: false,
|
throwOnError: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,13 +22,6 @@ import TomSelect from "tom-select";
|
||||||
import katex from "katex";
|
import katex from "katex";
|
||||||
import "katex/dist/katex.css";
|
import "katex/dist/katex.css";
|
||||||
|
|
||||||
|
|
||||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
|
||||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
|
||||||
|
|
||||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
|
||||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
|
||||||
|
|
||||||
/* stimulusFetch: 'lazy' */
|
/* stimulusFetch: 'lazy' */
|
||||||
export default class extends Controller
|
export default class extends Controller
|
||||||
{
|
{
|
||||||
|
@ -60,10 +53,7 @@ export default class extends Controller
|
||||||
connect() {
|
connect() {
|
||||||
const settings = {
|
const settings = {
|
||||||
plugins: {
|
plugins: {
|
||||||
'autoselect_typed': {},
|
clear_button:{}
|
||||||
'click_to_edit': {},
|
|
||||||
'clear_button': {},
|
|
||||||
'restore_on_backspace': {}
|
|
||||||
},
|
},
|
||||||
persistent: false,
|
persistent: false,
|
||||||
maxItems: 1,
|
maxItems: 1,
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
|
||||||
|
|
||||||
export default class extends Controller
|
|
||||||
{
|
|
||||||
static targets = ['link', 'mode', 'otherSelect'];
|
|
||||||
static values = {
|
|
||||||
targetId: Number,
|
|
||||||
};
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
const link = this.linkTarget;
|
|
||||||
const other_select = this.otherSelectTarget;
|
|
||||||
|
|
||||||
//Extract the mode using the mode radio buttons (we filter the array to get the checked one)
|
|
||||||
const mode = (this.modeTargets.filter((e)=>e.checked))[0].value;
|
|
||||||
|
|
||||||
if (other_select.value === '') {
|
|
||||||
link.classList.add('disabled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Extract href template from data attribute on link target
|
|
||||||
let href = link.getAttribute('data-href-template');
|
|
||||||
|
|
||||||
let target, other;
|
|
||||||
if (mode === '1') {
|
|
||||||
target = this.targetIdValue;
|
|
||||||
other = other_select.value;
|
|
||||||
} else if (mode === '2') {
|
|
||||||
target = other_select.value;
|
|
||||||
other = this.targetIdValue;
|
|
||||||
} else {
|
|
||||||
throw 'Invalid mode';
|
|
||||||
}
|
|
||||||
|
|
||||||
//Replace placeholder with actual target id
|
|
||||||
href = href.replace('__target__', target);
|
|
||||||
//Replace placeholder with selected value of the select (the event sender)
|
|
||||||
href = href.replace('__other__', other);
|
|
||||||
|
|
||||||
//Assign new href to link
|
|
||||||
link.setAttribute('href', href);
|
|
||||||
//Make link clickable
|
|
||||||
link.classList.remove('disabled');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,7 @@
|
||||||
<?php
|
|
||||||
/*
|
/*
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
*
|
*
|
||||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -18,13 +17,13 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
import {Controller} from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
export default class extends Controller
|
||||||
namespace App\Repository\UserSystem;
|
|
||||||
|
|
||||||
use Doctrine\ORM\EntityRepository;
|
|
||||||
|
|
||||||
class ApiTokenRepository extends EntityRepository
|
|
||||||
{
|
{
|
||||||
|
connect() {
|
||||||
|
this.element.onclick = function() {
|
||||||
|
window.u2fauth.register();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -99,25 +99,10 @@ label:not(.form-check-label, .custom-control-label) {
|
||||||
|
|
||||||
form .col-form-label.required:after, form label.required:after {
|
form .col-form-label.required:after, form label.required:after {
|
||||||
bottom: 4px;
|
bottom: 4px;
|
||||||
color: var(--bs-secondary-color);
|
color: var(--bs-dark);
|
||||||
content: "\2022";
|
content: "\2022";
|
||||||
filter: opacity(75%);
|
filter: opacity(75%);
|
||||||
position: relative;
|
position: relative;
|
||||||
right: -2px;
|
right: -2px;
|
||||||
z-index: 700;
|
z-index: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
/****************************************
|
|
||||||
* HTML diff styling
|
|
||||||
****************************************/
|
|
||||||
|
|
||||||
/* Insertations are marked with green background and bold */
|
|
||||||
ins {
|
|
||||||
background-color: #95f095;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
del {
|
|
||||||
background-color: #f09595;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
|
@ -1,11 +1,7 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
*
|
*
|
||||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -20,12 +16,22 @@ declare(strict_types=1);
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
namespace App\Entity\LabelSystem;
|
|
||||||
|
|
||||||
enum LabelProcessMode: string
|
.darkmode-layer {
|
||||||
{
|
z-index: 2020;
|
||||||
/** Use placeholders like [[PLACEHOLDER]] which gets replaced with content */
|
}
|
||||||
case PLACEHOLDER = 'html';
|
|
||||||
/** Interpret the given lines as twig template */
|
/** If darkmode is enabled revert the blening for images and videos, as these should be shown not inverted */
|
||||||
case TWIG = 'twig';
|
.darkmode--activated img,
|
||||||
|
.darkmode--activated video,
|
||||||
|
.darkmode--activated object {
|
||||||
|
mix-blend-mode: difference;
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkmode--activated .hoverpic:hover {
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-ic-logos img {
|
||||||
|
mix-blend-mode: normal;
|
||||||
}
|
}
|
|
@ -67,6 +67,7 @@ ul.structural_link {
|
||||||
padding-bottom: 7px;
|
padding-bottom: 7px;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Display list items side by side */
|
/* Display list items side by side */
|
||||||
|
@ -78,7 +79,7 @@ ul.structural_link li {
|
||||||
/* Add a slash symbol (/) before/behind each list item */
|
/* Add a slash symbol (/) before/behind each list item */
|
||||||
ul.structural_link li+li:before {
|
ul.structural_link li+li:before {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
color: var(--bs-tertiary-color);
|
color: grey;
|
||||||
/*content: "/\00a0";*/
|
/*content: "/\00a0";*/
|
||||||
font-family: "Font Awesome 5 Free";
|
font-family: "Font Awesome 5 Free";
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
@ -88,13 +89,13 @@ ul.structural_link li+li:before {
|
||||||
|
|
||||||
/* Add a color to all links inside the list */
|
/* Add a color to all links inside the list */
|
||||||
ul.structural_link li a {
|
ul.structural_link li a {
|
||||||
color: var(--bs-link-color);
|
color: #0275d8;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add a color on mouse-over */
|
/* Add a color on mouse-over */
|
||||||
ul.structural_link li a:hover {
|
ul.structural_link li a:hover {
|
||||||
color: var(--bs-link-hover-color);
|
color: #01447e;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,10 +113,3 @@ ul.structural_link li a:hover {
|
||||||
background-color: var(--bs-success);
|
background-color: var(--bs-success);
|
||||||
border-color: var(--bs-success);
|
border-color: var(--bs-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
/***********************************************
|
|
||||||
* Katex rendering with same height as text
|
|
||||||
***********************************************/
|
|
||||||
.katex-same-height-as-text .katex {
|
|
||||||
font-size: 1.0em;
|
|
||||||
}
|
|
|
@ -51,6 +51,7 @@
|
||||||
.part-table-image {
|
.part-table-image {
|
||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.part-info-image {
|
.part-info-image {
|
||||||
|
|
|
@ -78,6 +78,8 @@ body {
|
||||||
overflow: -moz-scrollbars-none;
|
overflow: -moz-scrollbars-none;
|
||||||
/* Use standard version for hiding the scrollbar */
|
/* Use standard version for hiding the scrollbar */
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
background-color: var(--light);
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar-container {
|
#sidebar-container {
|
||||||
|
@ -108,8 +110,8 @@ body {
|
||||||
.back-to-top {
|
.back-to-top {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 60px;
|
bottom: 20px;
|
||||||
right: 40px;
|
right: 20px;
|
||||||
display:none;
|
display:none;
|
||||||
z-index: 1030;
|
z-index: 1030;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,10 @@ table.dataTable > tbody > tr.selected > td > a {
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-footer-table {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
table.dataTable {
|
table.dataTable {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
@ -80,24 +84,14 @@ th.select-checkbox {
|
||||||
* Datatables definitions/overrides
|
* Datatables definitions/overrides
|
||||||
********************************************************************/
|
********************************************************************/
|
||||||
|
|
||||||
.dt-length {
|
.dataTables_length {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fix datatables select-checkbox position */
|
/** Fix datatables select-checkbox position */
|
||||||
table.dataTable tr.selected td.select-checkbox:after
|
table.dataTable tr.selected td.select-checkbox:after
|
||||||
{
|
{
|
||||||
margin-top: -20px !important;
|
margin-top: -28px !important;
|
||||||
}
|
|
||||||
|
|
||||||
/** Show pagination right aligned */
|
|
||||||
.dt-paging .pagination {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Fix table row coloring */
|
|
||||||
table.table.dataTable > :not(caption) > * > * {
|
|
||||||
background-color: var(--bs-table-bg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -110,3 +104,42 @@ Classes for Datatables export
|
||||||
.export-helper{
|
.export-helper{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/******************************************************
|
||||||
|
* Styling for the select all checkbox in the parts table
|
||||||
|
* Should match the styling of the select checkbox
|
||||||
|
******************************************************/
|
||||||
|
table.dataTable > thead > tr > th.select-checkbox {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
table.dataTable > thead > tr > th.select-checkbox:before,
|
||||||
|
table.dataTable > thead > tr > th.select-checkbox:after {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 1.2em;
|
||||||
|
left: 50%;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
table.dataTable > thead > tr > th.select-checkbox:before {
|
||||||
|
content: " ";
|
||||||
|
margin-top: -5px;
|
||||||
|
margin-left: -6px;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
table.dataTable > thead > tr.selected > th.select-checkbox:after {
|
||||||
|
content: "✓";
|
||||||
|
font-size: 20px;
|
||||||
|
margin-top: -23px;
|
||||||
|
margin-left: -6px;
|
||||||
|
text-align: center;
|
||||||
|
/*text-shadow: 1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9; */
|
||||||
|
}
|
||||||
|
table.dataTable.compact > thead > tr > th.select-checkbox:before {
|
||||||
|
margin-top: -12px;
|
||||||
|
}
|
||||||
|
table.dataTable.compact > thead > tr.selected > th.select-checkbox:after {
|
||||||
|
margin-top: -16px;
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -24,8 +24,9 @@
|
||||||
/** Should be the same settings, as in label_style.css */
|
/** Should be the same settings, as in label_style.css */
|
||||||
.ck-html-label .ck-content {
|
.ck-html-label .ck-content {
|
||||||
font-family: "DejaVu Sans Mono", monospace;
|
font-family: "DejaVu Sans Mono", monospace;
|
||||||
font-size: 12pt;
|
font-size: 12px;
|
||||||
line-height: 1.0;
|
line-height: 1.0;
|
||||||
|
font-size-adjust: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-html-label .ck-content p {
|
.ck-html-label .ck-content p {
|
||||||
|
@ -35,42 +36,3 @@
|
||||||
.ck-html-label .ck-content hr {
|
.ck-html-label .ck-content hr {
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/***********************************************
|
|
||||||
* Hide CKEditor powered by message
|
|
||||||
***********************************************/
|
|
||||||
.ck-powered-by {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***********************************************
|
|
||||||
* Use Bootstrap color vars for CKEditor
|
|
||||||
***********************************************/
|
|
||||||
:root {
|
|
||||||
--ck-color-base-foreground: var(--bs-secondary-bg);
|
|
||||||
--ck-color-base-background: var(--bs-body-bg);
|
|
||||||
--ck-color-base-border: var(--bs-border-color);
|
|
||||||
--ck-color-base-action: var(--bs-success);
|
|
||||||
--ck-color-base-focus: var(--bs-primary-border-subtle);
|
|
||||||
--ck-color-base-text: var(--bs-body-color);
|
|
||||||
--ck-color-base-active: var(--bs-primary-bg-subtle);
|
|
||||||
--ck-color-base-active-focus: var(--bs-primary);
|
|
||||||
--ck-color-base-error: var(--bs-danger);
|
|
||||||
|
|
||||||
/* Improve contrast between text and toolbar */
|
|
||||||
--ck-color-toolbar-background: var(--bs-tertiary-bg);
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
--ck-color-button-default-hover-background: var(--bs-secondary-bg);
|
|
||||||
--ck-color-button-default-active-background: var(--bs-secondary-bg);
|
|
||||||
|
|
||||||
--ck-color-button-on-background: var(--bs-body-bg);
|
|
||||||
--ck-color-button-on-hover-background: var(--bs-secondary-bg);
|
|
||||||
--ck-color-button-on-active-background: var(--bs-secondary-bg);
|
|
||||||
--ck-color-button-on-disabled-background: var(--bs-secondary-bg);
|
|
||||||
--ck-color-button-on-color: var(--bs-primary)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
/******************************************************************************************
|
|
||||||
* This styles the checkboxes of the select extension exactly like the ones in bootstrap 5
|
|
||||||
******************************************************************************************/
|
|
||||||
|
|
||||||
table.dataTable > tbody > tr > .selected {
|
|
||||||
background-color: var(--bs-primary-bg-subtle) !important;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
table.dataTable > tbody > tr > .dt-select {
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
table.dataTable > thead > tr > .dt-select {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
table.dataTable input.dt-select-checkbox {
|
|
||||||
--bs-form-check-bg: var(--bs-body-bg);
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
margin-top: 0.25em;
|
|
||||||
vertical-align: top;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
background-color: var(--bs-form-check-bg);
|
|
||||||
background-image: var(--bs-form-check-bg-image);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
background-size: contain;
|
|
||||||
border: var(--bs-border-width) solid var(--bs-border-color);
|
|
||||||
-webkit-print-color-adjust: exact;
|
|
||||||
color-adjust: exact;
|
|
||||||
print-color-adjust: exact;
|
|
||||||
border-radius: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.dataTable input.dt-select-checkbox:checked {
|
|
||||||
background-color: rgb(var(--bs-secondary-rgb));
|
|
||||||
border-color: rgb(var(--bs-secondary-rgb));
|
|
||||||
--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");
|
|
||||||
}
|
|
||||||
|
|
||||||
table.dataTable input.dt-select-checkbox:indeterminate {
|
|
||||||
background-color: rgb(var(--bs-secondary-rgb));
|
|
||||||
border-color: rgb(var(--bs-secondary-rgb));
|
|
||||||
--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div.dt-container span.select-info,
|
|
||||||
div.dt-container span.select-item {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: 640px) {
|
|
||||||
div.dt-container span.select-info,
|
|
||||||
div.dt-container span.select-item {
|
|
||||||
margin-left: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table.dataTable.table-sm tbody td.select-checkbox::before {
|
|
||||||
margin-top: -9px;
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,29 +18,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.tagsinput.ts-wrapper.multi .ts-control > div {
|
.tagsinput.ts-wrapper.multi .ts-control > div {
|
||||||
background: var(--bs-secondary-bg);
|
background: var(--bs-secondary);
|
||||||
color: var(--bs-body-color);
|
color: var(--bs-white);
|
||||||
}
|
|
||||||
|
|
||||||
/*********
|
|
||||||
* BS 5.3 compatible dark mode
|
|
||||||
***************/
|
|
||||||
|
|
||||||
.ts-dropdown .active {
|
|
||||||
background-color: var(--bs-secondary-bg) !important;
|
|
||||||
color: var(--bs-body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ts-dropdown, .ts-control, .ts-control input {
|
|
||||||
color: var(--bs-body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ts-dropdown, .ts-dropdown.form-control, .ts-dropdown.form-select {
|
|
||||||
background: var(--bs-body-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ts-dropdown .optgroup-header {
|
|
||||||
color: var(--bs-tertiary-color);
|
|
||||||
background: var(--bs-body-bg);
|
|
||||||
cursor: default;
|
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load diff
3
assets/fonts/dompdf/.gitignore
vendored
3
assets/fonts/dompdf/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
# Ignore font files
|
|
||||||
*.otf
|
|
||||||
*.ttf
|
|
|
@ -1 +0,0 @@
|
||||||
Put your font ttf files in this folder to make them available to the label generator.
|
|
|
@ -22,6 +22,7 @@
|
||||||
|
|
||||||
import '../css/app/layout.css';
|
import '../css/app/layout.css';
|
||||||
import '../css/app/helpers.css';
|
import '../css/app/helpers.css';
|
||||||
|
import '../css/app/darkmode.css';
|
||||||
import '../css/app/tables.css';
|
import '../css/app/tables.css';
|
||||||
import '../css/app/bs-overrides.css';
|
import '../css/app/bs-overrides.css';
|
||||||
import '../css/app/treeview.css';
|
import '../css/app/treeview.css';
|
||||||
|
@ -44,18 +45,4 @@ import "./register_events";
|
||||||
import "./tristate_checkboxes";
|
import "./tristate_checkboxes";
|
||||||
|
|
||||||
//Define jquery globally
|
//Define jquery globally
|
||||||
window.$ = window.jQuery = require("jquery");
|
window.$ = window.jQuery = require("jquery")
|
||||||
|
|
||||||
//Use the local WASM file for the ZXing library
|
|
||||||
import {
|
|
||||||
setZXingModuleOverrides,
|
|
||||||
} from "barcode-detector/pure";
|
|
||||||
import wasmFile from "../../node_modules/zxing-wasm/dist/reader/zxing_reader.wasm";
|
|
||||||
setZXingModuleOverrides({
|
|
||||||
locateFile: (path, prefix) => {
|
|
||||||
if (path.endsWith(".wasm")) {
|
|
||||||
return wasmFile;
|
|
||||||
}
|
|
||||||
return prefix + path;
|
|
||||||
},
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load diff
|
@ -47,8 +47,7 @@
|
||||||
method: config.method,
|
method: config.method,
|
||||||
data: {
|
data: {
|
||||||
_dt: config.name,
|
_dt: config.name,
|
||||||
_init: true,
|
_init: true
|
||||||
order: config.initial_order ?? undefined,
|
|
||||||
}
|
}
|
||||||
}).done(function(data) {
|
}).done(function(data) {
|
||||||
var baseState;
|
var baseState;
|
||||||
|
@ -73,17 +72,6 @@
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
request._dt = config.name;
|
request._dt = config.name;
|
||||||
|
|
||||||
//Try to resolve the original column index when the column was reordered (using the ColReorder plugin)
|
|
||||||
//Only do this when _ColReorder_iOrigCol is available
|
|
||||||
if (settings.aoColumns && settings.aoColumns.length && settings.aoColumns[0]._ColReorder_iOrigCol !== undefined) {
|
|
||||||
if (request.order && request.order.length) {
|
|
||||||
request.order.forEach(function (order) {
|
|
||||||
order.column = settings.aoColumns[order.column]._ColReorder_iOrigCol;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax(typeof config.url === 'function' ? config.url(dt) : config.url, {
|
$.ajax(typeof config.url === 'function' ? config.url(dt) : config.url, {
|
||||||
method: config.method,
|
method: config.method,
|
||||||
data: request
|
data: request
|
||||||
|
@ -98,15 +86,6 @@
|
||||||
dtOpts = config.options(dtOpts);
|
dtOpts = config.options(dtOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Choose the column where the className contains "select-column" and apply the select extension to its render field
|
|
||||||
//Added for Part-DB
|
|
||||||
for (let column of dtOpts.columns) {
|
|
||||||
if (column.className && column.className.includes('dt-select')) {
|
|
||||||
column.render = $.fn.dataTable.render.select();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
root.html(data.template);
|
root.html(data.template);
|
||||||
dt = $('table', root).DataTable(dtOpts);
|
dt = $('table', root).DataTable(dtOpts);
|
||||||
if (config.state !== 'none') {
|
if (config.state !== 'none') {
|
||||||
|
|
|
@ -20,8 +20,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import {Dropdown} from "bootstrap";
|
import {Dropdown} from "bootstrap";
|
||||||
import ClipboardJS from "clipboard";
|
|
||||||
import {Modal} from "bootstrap";
|
|
||||||
|
|
||||||
class RegisterEventHelper {
|
class RegisterEventHelper {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -29,14 +27,7 @@ class RegisterEventHelper {
|
||||||
this.configureDropdowns();
|
this.configureDropdowns();
|
||||||
this.registerSpecialCharInput();
|
this.registerSpecialCharInput();
|
||||||
|
|
||||||
//Initialize ClipboardJS
|
|
||||||
this.registerLoadHandler(() => {
|
|
||||||
new ClipboardJS('.btn');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.registerModalDropRemovalOnFormSubmit();
|
this.registerModalDropRemovalOnFormSubmit();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerModalDropRemovalOnFormSubmit() {
|
registerModalDropRemovalOnFormSubmit() {
|
||||||
|
@ -46,15 +37,6 @@ class RegisterEventHelper {
|
||||||
if (back_drop) {
|
if (back_drop) {
|
||||||
back_drop.remove();
|
back_drop.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
//Remove scroll-lock if it is still active
|
|
||||||
if (document.body.classList.contains('modal-open')) {
|
|
||||||
document.body.classList.remove('modal-open');
|
|
||||||
|
|
||||||
//Remove the padding-right and overflow:hidden from the body
|
|
||||||
document.body.style.paddingRight = '';
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,16 +59,13 @@ class RegisterEventHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
registerTooltips() {
|
registerTooltips() {
|
||||||
const handler = () => {
|
this.registerLoadHandler(() => {
|
||||||
$(".tooltip").remove();
|
$(".tooltip").remove();
|
||||||
//Exclude dropdown buttons from tooltips, otherwise we run into endless errors from bootstrap (bootstrap.esm.js:614 Bootstrap doesn't allow more than one instance per element. Bound instance: bs.dropdown.)
|
//Exclude dropdown buttons from tooltips, otherwise we run into endless errors from bootstrap (bootstrap.esm.js:614 Bootstrap doesn't allow more than one instance per element. Bound instance: bs.dropdown.)
|
||||||
$('a[title], label[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i[title], small[title]')
|
$('a[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i.fas[title]')
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
.tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'});
|
.tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'});
|
||||||
};
|
});
|
||||||
|
|
||||||
this.registerLoadHandler(handler);
|
|
||||||
document.addEventListener('dt:loaded', handler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSpecialCharInput() {
|
registerSpecialCharInput() {
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import {Tab, Dropdown, Collapse} from "bootstrap";
|
import {Tab, Dropdown} from "bootstrap";
|
||||||
import tab from "bootstrap/js/src/tab";
|
import tab from "bootstrap/js/src/tab";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,7 +54,6 @@ class TabRememberHelper {
|
||||||
const first_element = merged[0] ?? null;
|
const first_element = merged[0] ?? null;
|
||||||
if(first_element) {
|
if(first_element) {
|
||||||
this.revealElementOnTab(first_element);
|
this.revealElementOnTab(first_element);
|
||||||
this.revealElementInCollapse(first_element);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,20 +62,10 @@ class TabRememberHelper {
|
||||||
* @param event
|
* @param event
|
||||||
*/
|
*/
|
||||||
onInvalid(event) {
|
onInvalid(event) {
|
||||||
this.revealElementInCollapse(event.target);
|
|
||||||
this.revealElementOnTab(event.target);
|
this.revealElementOnTab(event.target);
|
||||||
this.revealElementInDropdown(event.target);
|
this.revealElementInDropdown(event.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
revealElementInCollapse(element) {
|
|
||||||
let collapse = element.closest('.collapse');
|
|
||||||
|
|
||||||
if(collapse) {
|
|
||||||
let bs_collapse = Collapse.getOrCreateInstance(collapse);
|
|
||||||
bs_collapse.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
revealElementInDropdown(element) {
|
revealElementInDropdown(element) {
|
||||||
let dropdown = element.closest('.dropdown-menu');
|
let dropdown = element.closest('.dropdown-menu');
|
||||||
|
|
||||||
|
|
|
@ -21,13 +21,8 @@
|
||||||
|
|
||||||
class WebauthnTFA {
|
class WebauthnTFA {
|
||||||
|
|
||||||
_b64UrlSafeEncode = (str) => {
|
|
||||||
const b64 = btoa(str);
|
|
||||||
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decodes a Base64Url string
|
// Decodes a Base64Url string
|
||||||
_b64UrlSafeDecode = (input) => {
|
_base64UrlDecode = (input) => {
|
||||||
input = input
|
input = input
|
||||||
.replace(/-/g, '+')
|
.replace(/-/g, '+')
|
||||||
.replace(/_/g, '/');
|
.replace(/_/g, '/');
|
||||||
|
@ -44,16 +39,13 @@ class WebauthnTFA {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Converts an array of bytes into a Base64Url string
|
// Converts an array of bytes into a Base64Url string
|
||||||
_arrayToBase64String = (a) => {
|
_arrayToBase64String = (a) => btoa(String.fromCharCode(...a));
|
||||||
const str = String.fromCharCode(...a);
|
|
||||||
return this._b64UrlSafeEncode(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepares the public key options object returned by the Webauthn Framework
|
// Prepares the public key options object returned by the Webauthn Framework
|
||||||
_preparePublicKeyOptions = publicKey => {
|
_preparePublicKeyOptions = publicKey => {
|
||||||
//Convert challenge from Base64Url string to Uint8Array
|
//Convert challenge from Base64Url string to Uint8Array
|
||||||
publicKey.challenge = Uint8Array.from(
|
publicKey.challenge = Uint8Array.from(
|
||||||
this._b64UrlSafeDecode(publicKey.challenge),
|
this._base64UrlDecode(publicKey.challenge),
|
||||||
c => c.charCodeAt(0)
|
c => c.charCodeAt(0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -75,7 +67,7 @@ class WebauthnTFA {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
id: Uint8Array.from(
|
id: Uint8Array.from(
|
||||||
this._b64UrlSafeDecode(data.id),
|
this._base64UrlDecode(data.id),
|
||||||
c => c.charCodeAt(0)
|
c => c.charCodeAt(0)
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -89,7 +81,7 @@ class WebauthnTFA {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
id: Uint8Array.from(
|
id: Uint8Array.from(
|
||||||
this._b64UrlSafeDecode(data.id),
|
this._base64UrlDecode(data.id),
|
||||||
c => c.charCodeAt(0)
|
c => c.charCodeAt(0)
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
/**
|
|
||||||
* Autoselect Typed plugin for Tomselect
|
|
||||||
*
|
|
||||||
* This plugin allows automatically selecting an option matching the typed text when the Tomselect element goes out of
|
|
||||||
* focus (is blurred) and/or when the delimiter is typed.
|
|
||||||
*
|
|
||||||
* #select_on_blur option
|
|
||||||
* Tomselect natively supports the "createOnBlur" option. This option picks up any remaining text in the input field
|
|
||||||
* and uses it to create a new option and selects that option. It does behave a bit strangely though, in that it will
|
|
||||||
* not select an already existing option when the input is blurred, so if you typed something that matches an option in
|
|
||||||
* the list and then click outside the box (without pressing enter) the entered text is just removed (unless you have
|
|
||||||
* allow duplicates on in which case it will create a new option).
|
|
||||||
* This plugin fixes that, such that Tomselect will first try to select an option matching the remaining uncommitted
|
|
||||||
* text and only when no matching option is found tries to create a new one (if createOnBlur and create is on)
|
|
||||||
*
|
|
||||||
* #select_on_delimiter option
|
|
||||||
* Normally when typing the delimiter (space by default) Tomselect will try to create a new option (and select it) (if
|
|
||||||
* create is on), but if the typed text matches an option (and allow duplicates is off) it refuses to react at all until
|
|
||||||
* you press enter. With this option, the delimiter will also allow selecting an option, not just creating it.
|
|
||||||
*/
|
|
||||||
function select_current_input(self){
|
|
||||||
if(self.isLocked){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const val = self.inputValue()
|
|
||||||
//Do nothing if the input is empty
|
|
||||||
if (!val) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.options[val]) {
|
|
||||||
self.addItem(val)
|
|
||||||
self.setTextboxValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function(plugin_options_) {
|
|
||||||
const plugin_options = Object.assign({
|
|
||||||
//Autoselect the typed text when the input element goes out of focus
|
|
||||||
select_on_blur: true,
|
|
||||||
//Autoselect the typed text when the delimiter is typed
|
|
||||||
select_on_delimiter: true,
|
|
||||||
}, plugin_options_);
|
|
||||||
|
|
||||||
const self = this
|
|
||||||
|
|
||||||
if(plugin_options.select_on_blur) {
|
|
||||||
this.hook("before", "onBlur", function () {
|
|
||||||
select_current_input(self)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(plugin_options.select_on_delimiter) {
|
|
||||||
this.hook("before", "onKeyPress", function (e) {
|
|
||||||
const character = String.fromCharCode(e.keyCode || e.which);
|
|
||||||
if (self.settings.mode === 'multi' && character === self.settings.delimiter) {
|
|
||||||
select_current_input(self)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
/**
|
|
||||||
* click_to_edit plugin for Tomselect
|
|
||||||
*
|
|
||||||
* This plugin allows editing (and selecting text in) any selected item by clicking it.
|
|
||||||
*
|
|
||||||
* Usually, when the user typed some text and created an item in Tomselect that item cannot be edited anymore. To make
|
|
||||||
* a change, the item has to be deleted and retyped completely. There is also generally no way to copy text out of a
|
|
||||||
* tomselect item. The "restore_on_backspace" plugin improves that somewhat, by allowing the user to edit an item after
|
|
||||||
* pressing backspace. However, it is somewhat confusing to first have to focus the field an then hit backspace in order
|
|
||||||
* to copy a piece of text. It may also not be immediately obvious for editing.
|
|
||||||
* This plugin transforms an item into editable text when it is clicked, e.g. when the user tries to place the caret
|
|
||||||
* within an item or when they try to drag across the text to highlight it.
|
|
||||||
* It also plays nice with the remove_button plugin which still removes (deselects) an option entirely.
|
|
||||||
*
|
|
||||||
* It is recommended to also enable the autoselect_typed plugin when using this plugin. Without it, the text in the
|
|
||||||
* input field (i.e. the item that was just clicked) is lost when the user clicks outside the field. Also, when the user
|
|
||||||
* clicks an option (making it text) and then tries to enter another one by entering the delimiter (e.g. space) nothing
|
|
||||||
* happens until enter is pressed or the text is changed from what it was.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a dom element from either a dom query string, jQuery object, a dom element or html string
|
|
||||||
* https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
|
|
||||||
*
|
|
||||||
* param query should be {}
|
|
||||||
*/
|
|
||||||
const getDom = query => {
|
|
||||||
if (query.jquery) {
|
|
||||||
return query[0];
|
|
||||||
}
|
|
||||||
if (query instanceof HTMLElement) {
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
if (isHtmlString(query)) {
|
|
||||||
var tpl = document.createElement('template');
|
|
||||||
tpl.innerHTML = query.trim(); // Never return a text node of whitespace as the result
|
|
||||||
return tpl.content.firstChild;
|
|
||||||
}
|
|
||||||
return document.querySelector(query);
|
|
||||||
};
|
|
||||||
const isHtmlString = arg => {
|
|
||||||
if (typeof arg === 'string' && arg.indexOf('<') > -1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
function plugin(plugin_options_) {
|
|
||||||
const self = this
|
|
||||||
|
|
||||||
const plugin_options = Object.assign({
|
|
||||||
//If there is unsubmitted text in the input field, should that text be automatically used to select a matching
|
|
||||||
//element? If this is off, clicking on item1 and then clicking on item2 will result in item1 being deselected
|
|
||||||
auto_select_before_edit: true,
|
|
||||||
//If there is unsubmitted text in the input field, should that text be automatically used to create a matching
|
|
||||||
//element if no matching element was found or auto_select_before_edit is off?
|
|
||||||
auto_create_before_edit: true,
|
|
||||||
//customize this function to change which text the item is replaced with when clicking on it
|
|
||||||
text: option => {
|
|
||||||
return option[self.settings.labelField];
|
|
||||||
}
|
|
||||||
}, plugin_options_);
|
|
||||||
|
|
||||||
|
|
||||||
self.hook('after', 'setupTemplates', () => {
|
|
||||||
const orig_render_item = self.settings.render.item;
|
|
||||||
self.settings.render.item = (data, escape) => {
|
|
||||||
const item = getDom(orig_render_item.call(self, data, escape));
|
|
||||||
|
|
||||||
item.addEventListener('click', evt => {
|
|
||||||
if (self.isLocked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const val = self.inputValue();
|
|
||||||
|
|
||||||
if (self.options[val]) {
|
|
||||||
self.addItem(val)
|
|
||||||
} else if (self.settings.create) {
|
|
||||||
self.createItem();
|
|
||||||
}
|
|
||||||
const option = self.options[item.dataset.value]
|
|
||||||
self.setTextboxValue(plugin_options.text.call(self, option));
|
|
||||||
self.focus();
|
|
||||||
self.removeItem(item);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
export { plugin as default };
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { localeFallbacks } from '../var/translations/configuration';
|
|
||||||
import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-translator';
|
|
||||||
/*
|
|
||||||
* This file is part of the Symfony UX Translator package.
|
|
||||||
*
|
|
||||||
* If folder "../var/translations" does not exist, or some translations are missing,
|
|
||||||
* you must warmup your Symfony cache to refresh JavaScript translations.
|
|
||||||
*
|
|
||||||
* If you use TypeScript, you can rename this file to "translator.ts" to take advantage of types checking.
|
|
||||||
*/
|
|
||||||
|
|
||||||
setLocaleFallbacks(localeFallbacks);
|
|
||||||
|
|
||||||
export { trans };
|
|
||||||
|
|
||||||
export * from '../var/translations';
|
|
11
bin/console
11
bin/console
|
@ -4,17 +4,6 @@
|
||||||
use App\Kernel;
|
use App\Kernel;
|
||||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||||
|
|
||||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
|
||||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
|
||||||
}
|
|
||||||
|
|
||||||
//Increase xdebug.max_nesting_level to 1000 if required (see issue #411)
|
|
||||||
//Check if xdebug extension is active, and xdebug.max_nesting_level is set to 256 or lower
|
|
||||||
if (extension_loaded('xdebug') && ((int) ini_get('xdebug.max_nesting_level')) <= 256) {
|
|
||||||
//Increase xdebug.max_nesting_level to 1000
|
|
||||||
ini_set('xdebug.max_nesting_level', '1000');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,9 @@ if (!ini_get('date.timezone')) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
|
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
|
||||||
if (PHP_VERSION_ID >= 80000) {
|
|
||||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
|
||||||
} else {
|
|
||||||
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
|
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
|
||||||
require PHPUNIT_COMPOSER_INSTALL;
|
require PHPUNIT_COMPOSER_INSTALL;
|
||||||
PHPUnit\TextUI\Command::main();
|
PHPUnit\TextUI\Command::main();
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
|
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
|
||||||
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
|
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
|
||||||
|
|
|
@ -5,5 +5,4 @@ coverage:
|
||||||
status:
|
status:
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
threshold: 10%
|
threshold: 5%
|
||||||
target: 40%
|
|
159
composer.json
159
composer.json
|
@ -1,118 +1,106 @@
|
||||||
{
|
{
|
||||||
"name": "part-db/part-db-server",
|
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1",
|
"php": "^7.4 || ^8.0",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-dom": "*",
|
|
||||||
"ext-gd": "*",
|
"ext-gd": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"amphp/http-client": "^5.1",
|
"ext-dom": "*",
|
||||||
"api-platform/core": "^3.1",
|
|
||||||
"beberlei/doctrineextensions": "^1.2",
|
"beberlei/doctrineextensions": "^1.2",
|
||||||
"brick/math": "0.12.1 as 0.11.0",
|
"brick/math": "^0.8.15",
|
||||||
"composer/ca-bundle": "^1.5",
|
"composer/package-versions-deprecated": "1.11.99.4",
|
||||||
"composer/package-versions-deprecated": "^1.11.99.5",
|
"doctrine/annotations": "^1.6",
|
||||||
"doctrine/data-fixtures": "^2.0.0",
|
"doctrine/dbal": "^3.4.6",
|
||||||
"doctrine/dbal": "^4.0.0",
|
|
||||||
"doctrine/doctrine-bundle": "^2.0",
|
"doctrine/doctrine-bundle": "^2.0",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||||
"doctrine/orm": "^3.2.0",
|
"doctrine/orm": "^2.9",
|
||||||
"dompdf/dompdf": "^v3.0.0",
|
"dompdf/dompdf": "^2.0.0",
|
||||||
"erusev/parsedown": "^1.7",
|
"erusev/parsedown": "^1.7",
|
||||||
"florianv/swap": "^4.0",
|
"florianv/swap": "^4.0",
|
||||||
"florianv/swap-bundle": "dev-master",
|
"florianv/swap-bundle": "dev-master",
|
||||||
"gregwar/captcha-bundle": "^2.1.0",
|
"gregwar/captcha-bundle": "^2.1.0",
|
||||||
"hshn/base64-encoded-file": "^5.0",
|
"hslavich/oneloginsaml-bundle": "^2.10",
|
||||||
"jbtronics/2fa-webauthn": "^v2.2.0",
|
"jbtronics/2fa-webauthn": "^1.0.0",
|
||||||
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
|
|
||||||
"jfcherng/php-diff": "^6.14",
|
|
||||||
"knpuniversity/oauth2-client-bundle": "^2.15",
|
|
||||||
"league/csv": "^9.8.0",
|
"league/csv": "^9.8.0",
|
||||||
"league/html-to-markdown": "^5.0.1",
|
"league/html-to-markdown": "^5.0.1",
|
||||||
"liip/imagine-bundle": "^2.2",
|
"liip/imagine-bundle": "^2.2",
|
||||||
"nbgrp/onelogin-saml-bundle": "^1.3",
|
|
||||||
"nelexa/zip": "^4.0",
|
"nelexa/zip": "^4.0",
|
||||||
"nelmio/cors-bundle": "^2.3",
|
|
||||||
"nelmio/security-bundle": "^3.0",
|
"nelmio/security-bundle": "^3.0",
|
||||||
"nyholm/psr7": "^1.1",
|
"nyholm/psr7": "^1.1",
|
||||||
"omines/datatables-bundle": "^0.9.1",
|
"ocramius/proxy-manager": "2.2.*",
|
||||||
"paragonie/sodium_compat": "^1.21",
|
"omines/datatables-bundle": "^0.5.0",
|
||||||
"part-db/label-fonts": "^1.0",
|
"php-translation/symfony-bundle": "^0.13.0",
|
||||||
"rhukster/dom-sanitizer": "^1.0",
|
"phpdocumentor/reflection-docblock": "^5.2",
|
||||||
"runtime/frankenphp-symfony": "^0.2.0",
|
|
||||||
"s9e/text-formatter": "^2.1",
|
"s9e/text-formatter": "^2.1",
|
||||||
"scheb/2fa-backup-code": "^6.8.0",
|
"scheb/2fa-backup-code": "^5.13",
|
||||||
"scheb/2fa-bundle": "^6.8.0",
|
"scheb/2fa-bundle": "^5.13",
|
||||||
"scheb/2fa-google-authenticator": "^6.8.0",
|
"scheb/2fa-google-authenticator": "^5.13",
|
||||||
"scheb/2fa-trusted-device": "^6.8.0",
|
"scheb/2fa-trusted-device": "^5.13",
|
||||||
|
"sensio/framework-extra-bundle": "^6.1.1",
|
||||||
"shivas/versioning-bundle": "^4.0",
|
"shivas/versioning-bundle": "^4.0",
|
||||||
"spatie/db-dumper": "^3.3.1",
|
"spatie/db-dumper": "^2.21",
|
||||||
"symfony/apache-pack": "^1.0",
|
"symfony/apache-pack": "^1.0",
|
||||||
"symfony/asset": "6.4.*",
|
"symfony/asset": "5.4.*",
|
||||||
"symfony/console": "6.4.*",
|
"symfony/console": "5.4.*",
|
||||||
"symfony/css-selector": "6.4.*",
|
"symfony/dotenv": "5.4.*",
|
||||||
"symfony/dom-crawler": "6.4.*",
|
"symfony/expression-language": "5.4.*",
|
||||||
"symfony/dotenv": "6.4.*",
|
"symfony/flex": "^1.1",
|
||||||
"symfony/expression-language": "6.4.*",
|
"symfony/form": "5.4.*",
|
||||||
"symfony/flex": "^v2.3.1",
|
"symfony/framework-bundle": "5.4.*",
|
||||||
"symfony/form": "6.4.*",
|
"symfony/http-client": "5.4.*",
|
||||||
"symfony/framework-bundle": "6.4.*",
|
"symfony/http-kernel": "5.4.*",
|
||||||
"symfony/http-client": "6.4.*",
|
"symfony/mailer": "5.4.*",
|
||||||
"symfony/http-kernel": "6.4.*",
|
|
||||||
"symfony/mailer": "6.4.*",
|
|
||||||
"symfony/monolog-bundle": "^3.1",
|
"symfony/monolog-bundle": "^3.1",
|
||||||
"symfony/polyfill-php82": "^1.28",
|
"symfony/process": "5.4.*",
|
||||||
"symfony/process": "6.4.*",
|
"symfony/property-access": "5.4.*",
|
||||||
"symfony/property-access": "6.4.*",
|
"symfony/property-info": "5.4.*",
|
||||||
"symfony/property-info": "6.4.*",
|
"symfony/proxy-manager-bridge": "5.4.*",
|
||||||
"symfony/rate-limiter": "6.4.*",
|
"symfony/rate-limiter": "5.4.*",
|
||||||
"symfony/runtime": "6.4.*",
|
"symfony/runtime": "5.4.*",
|
||||||
"symfony/security-bundle": "6.4.*",
|
"symfony/security-bundle": "5.4.*",
|
||||||
"symfony/serializer": "6.4.*",
|
"symfony/serializer": "5.4.*",
|
||||||
"symfony/string": "6.4.*",
|
"symfony/translation": "5.4.*",
|
||||||
"symfony/translation": "6.4.*",
|
"symfony/twig-bundle": "5.4.*",
|
||||||
"symfony/twig-bundle": "6.4.*",
|
|
||||||
"symfony/ux-translator": "^2.10",
|
|
||||||
"symfony/ux-turbo": "^2.0",
|
"symfony/ux-turbo": "^2.0",
|
||||||
"symfony/validator": "6.4.*",
|
"symfony/validator": "5.4.*",
|
||||||
"symfony/web-link": "6.4.*",
|
"symfony/web-link": "5.4.*",
|
||||||
"symfony/webpack-encore-bundle": "^v2.0.1",
|
"symfony/webpack-encore-bundle": "^1.1",
|
||||||
"symfony/yaml": "6.4.*",
|
"symfony/yaml": "5.4.*",
|
||||||
"tecnickcom/tc-lib-barcode": "^2.1.4",
|
"tecnickcom/tc-lib-barcode": "^1.15",
|
||||||
"twig/cssinliner-extra": "^3.0",
|
"twig/cssinliner-extra": "^3.0",
|
||||||
"twig/extra-bundle": "^3.8",
|
"twig/extra-bundle": "^3.0",
|
||||||
"twig/html-extra": "^3.8",
|
"twig/html-extra": "^3.0",
|
||||||
"twig/inky-extra": "^3.0",
|
"twig/inky-extra": "^3.0",
|
||||||
"twig/intl-extra": "^3.8",
|
"twig/intl-extra": "^3.0",
|
||||||
"twig/markdown-extra": "^3.8",
|
"twig/markdown-extra": "^3.0",
|
||||||
"twig/string-extra": "^3.8",
|
"web-auth/webauthn-symfony-bundle": "^3.3",
|
||||||
"web-auth/webauthn-symfony-bundle": "^4.0.0"
|
"webmozart/assert": "^1.4",
|
||||||
|
"doctrine/data-fixtures": "^1.6.6"
|
||||||
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"dama/doctrine-test-bundle": "^v8.0.0",
|
"dama/doctrine-test-bundle": "^7.0",
|
||||||
"doctrine/doctrine-fixtures-bundle": "^4.0.0",
|
"ekino/phpstan-banned-code": "^v1.0.0",
|
||||||
"ekino/phpstan-banned-code": "^v3.0.0",
|
|
||||||
"jbtronics/translation-editor-bundle": "^1.0",
|
|
||||||
"phpstan/extension-installer": "^1.0",
|
"phpstan/extension-installer": "^1.0",
|
||||||
"phpstan/phpstan": "^2.0.4",
|
"phpstan/phpstan": "^1.4.7",
|
||||||
"phpstan/phpstan-doctrine": "^2.0.1",
|
"phpstan/phpstan-doctrine": "^1.2.11",
|
||||||
"phpstan/phpstan-strict-rules": "^2.0.1",
|
"phpstan/phpstan-symfony": "^1.1.7",
|
||||||
"phpstan/phpstan-symfony": "^2.0.0",
|
"psalm/plugin-symfony": "^v5.0.1",
|
||||||
"phpunit/phpunit": "^9.5",
|
|
||||||
"rector/rector": "^2.0.4",
|
|
||||||
"roave/security-advisories": "dev-latest",
|
"roave/security-advisories": "dev-latest",
|
||||||
"symfony/browser-kit": "6.4.*",
|
"symfony/browser-kit": "^5.2",
|
||||||
"symfony/debug-bundle": "6.4.*",
|
"symfony/css-selector": "^5.2",
|
||||||
|
"symfony/debug-bundle": "^5.2",
|
||||||
"symfony/maker-bundle": "^1.13",
|
"symfony/maker-bundle": "^1.13",
|
||||||
"symfony/phpunit-bridge": "6.4.*",
|
"symfony/phpunit-bridge": "5.4.*",
|
||||||
"symfony/stopwatch": "6.4.*",
|
"symfony/stopwatch": "^5.2",
|
||||||
"symfony/web-profiler-bundle": "6.4.*",
|
"symfony/web-profiler-bundle": "^5.2",
|
||||||
"symplify/easy-coding-standard": "^12.0"
|
"symplify/easy-coding-standard": "^11.0",
|
||||||
|
"vimeo/psalm": "^5.6.0",
|
||||||
|
"doctrine/doctrine-fixtures-bundle": "^3.2"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-bcmath": "Used to improve price calculation performance",
|
"ext-bcmath": "Used to improve price calculation performance",
|
||||||
|
@ -123,7 +111,7 @@
|
||||||
"*": "dist"
|
"*": "dist"
|
||||||
},
|
},
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "8.1.0"
|
"php": "7.4.0"
|
||||||
},
|
},
|
||||||
"sort-packages": true,
|
"sort-packages": true,
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
|
@ -155,7 +143,7 @@
|
||||||
"post-update-cmd": [
|
"post-update-cmd": [
|
||||||
"@auto-scripts"
|
"@auto-scripts"
|
||||||
],
|
],
|
||||||
"phpstan": "vendor/bin/phpstan analyse src --level 5 --memory-limit 1G"
|
"phpstan": "vendor/bin/phpstan analyse src --level 2 --memory-limit 1G"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"symfony/symfony": "*"
|
"symfony/symfony": "*"
|
||||||
|
@ -163,8 +151,9 @@
|
||||||
"extra": {
|
"extra": {
|
||||||
"symfony": {
|
"symfony": {
|
||||||
"allow-contrib": false,
|
"allow-contrib": false,
|
||||||
"require": "6.4.*",
|
"require": "5.4.*"
|
||||||
"docker": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"repositories": [
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
13817
composer.lock
generated
13817
composer.lock
generated
File diff suppressed because it is too large
Load diff
23
config/bootstrap.php
Normal file
23
config/bootstrap.php
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Dotenv\Dotenv;
|
||||||
|
|
||||||
|
require dirname(__DIR__).'/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (!class_exists(Dotenv::class)) {
|
||||||
|
throw new LogicException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load cached env vars if the .env.local.php file exists
|
||||||
|
// Run "composer dump-env prod" to create it (requires symfony/flex >=1.2)
|
||||||
|
if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) {
|
||||||
|
(new Dotenv(false))->populate($env);
|
||||||
|
} else {
|
||||||
|
// load all the .env files
|
||||||
|
(new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env');
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SERVER += $_ENV;
|
||||||
|
$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
|
||||||
|
$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];
|
||||||
|
$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||||
|
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
|
@ -18,18 +19,13 @@ return [
|
||||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||||
Gregwar\CaptchaBundle\GregwarCaptchaBundle::class => ['all' => true],
|
Gregwar\CaptchaBundle\GregwarCaptchaBundle::class => ['all' => true],
|
||||||
|
Translation\Bundle\TranslationBundle::class => ['all' => true],
|
||||||
Florianv\SwapBundle\FlorianvSwapBundle::class => ['all' => true],
|
Florianv\SwapBundle\FlorianvSwapBundle::class => ['all' => true],
|
||||||
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
|
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
|
||||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
||||||
Jbtronics\TFAWebauthn\TFAWebauthnBundle::class => ['all' => true],
|
Jbtronics\TFAWebauthn\TFAWebauthnBundle::class => ['all' => true],
|
||||||
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
|
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
|
||||||
|
SpomkyLabs\CborBundle\SpomkyLabsCborBundle::class => ['all' => true],
|
||||||
Webauthn\Bundle\WebauthnBundle::class => ['all' => true],
|
Webauthn\Bundle\WebauthnBundle::class => ['all' => true],
|
||||||
Nbgrp\OneloginSamlBundle\NbgrpOneloginSamlBundle::class => ['all' => true],
|
Hslavich\OneloginSamlBundle\HslavichOneloginSamlBundle::class => ['all' => true],
|
||||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
|
||||||
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
|
||||||
Jbtronics\DompdfFontLoaderBundle\DompdfFontLoaderBundle::class => ['all' => true],
|
|
||||||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
|
||||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
|
||||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
|
||||||
Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true],
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
api_platform:
|
|
||||||
title: 'Part-DB API'
|
|
||||||
description: 'API of Part-DB'
|
|
||||||
version: '0.1.0'
|
|
||||||
|
|
||||||
formats:
|
|
||||||
jsonld: ['application/ld+json']
|
|
||||||
json: ['application/json']
|
|
||||||
jsonapi: ['application/vnd.api+json']
|
|
||||||
|
|
||||||
docs_formats:
|
|
||||||
jsonld: ['application/ld+json']
|
|
||||||
jsonopenapi: ['application/vnd.openapi+json']
|
|
||||||
html: ['text/html']
|
|
||||||
json: ['application/vnd.openapi+json']
|
|
||||||
|
|
||||||
swagger:
|
|
||||||
api_keys:
|
|
||||||
# overridden in OpenApiFactoryDecorator
|
|
||||||
access_token:
|
|
||||||
name: Authorization
|
|
||||||
type: header
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
# TODO: Change this to true later. In the moment it is false, because we use the session in somewhere
|
|
||||||
stateless: false
|
|
||||||
cache_headers:
|
|
||||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
|
||||||
extra_properties:
|
|
||||||
standard_put: true
|
|
||||||
rfc_7807_compliant_errors: true
|
|
||||||
|
|
||||||
pagination_client_items_per_page: true # Allow clients to override the default items per page
|
|
||||||
|
|
||||||
keep_legacy_inflector: false
|
|
||||||
# Need to be true, or some tests will fail
|
|
||||||
use_symfony_listeners: true
|
|
||||||
|
|
||||||
serializer:
|
|
||||||
# Change this to false later, to remove the hydra prefix on the API
|
|
||||||
hydra_prefix: true
|
|
|
@ -20,6 +20,3 @@ framework:
|
||||||
tree.cache:
|
tree.cache:
|
||||||
adapter: cache.app
|
adapter: cache.app
|
||||||
tags: true
|
tags: true
|
||||||
|
|
||||||
info_provider.cache:
|
|
||||||
adapter: cache.app
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
when@test:
|
|
||||||
dama_doctrine_test:
|
|
||||||
enable_static_connection: true
|
|
||||||
enable_static_meta_data_cache: true
|
|
||||||
enable_static_query_cache: true
|
|
|
@ -8,14 +8,15 @@ datatables:
|
||||||
|
|
||||||
# Set options, as documented at https://datatables.net/reference/option/
|
# Set options, as documented at https://datatables.net/reference/option/
|
||||||
options:
|
options:
|
||||||
lengthMenu : [[10, 25, 50, 100], [10, 25, 50, 100]] # We add the "All" option, when part tables are generated
|
lengthMenu : [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]
|
||||||
pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
|
pageLength: 50
|
||||||
dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>>
|
#dom: "<'row' <'col-sm-12' tr>><'row' <'col-sm-6'l><'col-sm-6 text-right'pif>>"
|
||||||
|
dom: " <'row'<'col mb-2 input-group' B l> <'col mb-2' <'pull-end' p>>>
|
||||||
<'card'
|
<'card'
|
||||||
rt
|
rt
|
||||||
<'card-footer card-footer-table text-muted' i >
|
<'card-footer card-footer-table text-muted' i >
|
||||||
>
|
>
|
||||||
<'row' <'col mt-2 input-group flex-nowrap' B l > <'col-auto mt-2' < p >>>"
|
<'row'<'col mt-2 input-group' B l> <'col mt-2' <'pull-right' p>>>"
|
||||||
pagingType: 'simple_numbers'
|
pagingType: 'simple_numbers'
|
||||||
searching: true
|
searching: true
|
||||||
stateSave: true
|
stateSave: true
|
||||||
|
|
5
config/packages/dev/php_translation.yaml
Normal file
5
config/packages/dev/php_translation.yaml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
translation:
|
||||||
|
symfony_profiler:
|
||||||
|
enabled: true
|
||||||
|
webui:
|
||||||
|
enabled: true
|
|
@ -2,62 +2,39 @@ doctrine:
|
||||||
dbal:
|
dbal:
|
||||||
url: '%env(resolve:DATABASE_URL)%'
|
url: '%env(resolve:DATABASE_URL)%'
|
||||||
|
|
||||||
# Required for DAMA doctrine test bundle
|
|
||||||
use_savepoints: true
|
|
||||||
|
|
||||||
# IMPORTANT: You MUST configure your server version,
|
# IMPORTANT: You MUST configure your server version,
|
||||||
# either here or in the DATABASE_URL env var (see .env file)
|
# either here or in the DATABASE_URL env var (see .env file)
|
||||||
|
|
||||||
types:
|
types:
|
||||||
# UTC datetimes
|
|
||||||
datetime:
|
datetime:
|
||||||
class: App\Doctrine\Types\UTCDateTimeType
|
class: App\Doctrine\Types\UTCDateTimeType
|
||||||
date:
|
date:
|
||||||
class: App\Doctrine\Types\UTCDateTimeType
|
class: App\Doctrine\Types\UTCDateTimeType
|
||||||
|
|
||||||
datetime_immutable:
|
|
||||||
class: App\Doctrine\Types\UTCDateTimeImmutableType
|
|
||||||
date_immutable:
|
|
||||||
class: App\Doctrine\Types\UTCDateTimeImmutableType
|
|
||||||
|
|
||||||
big_decimal:
|
big_decimal:
|
||||||
class: App\Doctrine\Types\BigDecimalType
|
class: App\Doctrine\Types\BigDecimalType
|
||||||
tinyint:
|
tinyint:
|
||||||
class: App\Doctrine\Types\TinyIntType
|
class: App\Doctrine\Types\TinyIntType
|
||||||
|
|
||||||
# This was removed in doctrine/orm 4.0 but we need it for the WebauthnKey entity
|
|
||||||
array:
|
|
||||||
class: App\Doctrine\Types\ArrayType
|
|
||||||
|
|
||||||
schema_filter: ~^(?!internal)~
|
schema_filter: ~^(?!internal)~
|
||||||
# Only enable this when needed
|
# Only enable this when needed
|
||||||
profiling_collect_backtrace: false
|
profiling_collect_backtrace: false
|
||||||
|
|
||||||
orm:
|
orm:
|
||||||
auto_generate_proxy_classes: true
|
auto_generate_proxy_classes: true
|
||||||
enable_lazy_ghost_objects: true
|
|
||||||
report_fields_where_declared: true
|
|
||||||
validate_xml_mapping: true
|
|
||||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||||
auto_mapping: true
|
auto_mapping: true
|
||||||
controller_resolver:
|
|
||||||
auto_mapping: true
|
|
||||||
mappings:
|
mappings:
|
||||||
App:
|
App:
|
||||||
type: attribute
|
|
||||||
is_bundle: false
|
is_bundle: false
|
||||||
|
type: annotation
|
||||||
dir: '%kernel.project_dir%/src/Entity'
|
dir: '%kernel.project_dir%/src/Entity'
|
||||||
prefix: 'App\Entity'
|
prefix: 'App\Entity'
|
||||||
alias: App
|
alias: App
|
||||||
|
|
||||||
dql:
|
dql:
|
||||||
string_functions:
|
string_functions:
|
||||||
regexp: App\Doctrine\Functions\Regexp
|
regexp: DoctrineExtensions\Query\Mysql\Regexp
|
||||||
field: DoctrineExtensions\Query\Mysql\Field
|
ifnull: DoctrineExtensions\Query\Mysql\IfNull
|
||||||
field2: App\Doctrine\Functions\Field2
|
|
||||||
natsort: App\Doctrine\Functions\Natsort
|
|
||||||
array_position: App\Doctrine\Functions\ArrayPosition
|
|
||||||
ilike: App\Doctrine\Functions\ILike
|
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
doctrine:
|
doctrine:
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
dompdf_font_loader:
|
|
||||||
auto_install: true
|
|
||||||
|
|
||||||
fonts:
|
|
||||||
unifont:
|
|
||||||
normal: "%kernel.project_dir%/vendor/part-db/label-fonts/fonts/unifont.ttf"
|
|
||||||
|
|
||||||
# Enable autodiscovery of fonts, so that font installation is much easier
|
|
||||||
autodiscovery:
|
|
||||||
paths:
|
|
||||||
- "%kernel.project_dir%/assets/fonts/dompdf"
|
|
|
@ -2,13 +2,8 @@
|
||||||
framework:
|
framework:
|
||||||
secret: '%env(APP_SECRET)%'
|
secret: '%env(APP_SECRET)%'
|
||||||
csrf_protection: true
|
csrf_protection: true
|
||||||
annotations: false
|
|
||||||
handle_all_throwables: true
|
|
||||||
|
|
||||||
# We set this header by ourselves, so we can disable it here
|
# Must be set to true, to enable the change of HTTP methhod via _method parameter, otherwise our delete routines does not work anymore
|
||||||
disallow_search_engine_index: false
|
|
||||||
|
|
||||||
# Must be set to true, to enable the change of HTTP method via _method parameter, otherwise our delete routines does not work anymore
|
|
||||||
# TODO: Rework delete routines to work without _method parameter as it is not recommended anymore (see https://github.com/symfony/symfony/issues/45278)
|
# TODO: Rework delete routines to work without _method parameter as it is not recommended anymore (see https://github.com/symfony/symfony/issues/45278)
|
||||||
http_method_override: true
|
http_method_override: true
|
||||||
|
|
||||||
|
@ -27,12 +22,16 @@ framework:
|
||||||
handler_id: null
|
handler_id: null
|
||||||
cookie_secure: auto
|
cookie_secure: auto
|
||||||
cookie_samesite: lax
|
cookie_samesite: lax
|
||||||
|
storage_factory_id: session.storage.factory.native
|
||||||
|
|
||||||
#esi: true
|
#esi: true
|
||||||
#fragments: true
|
#fragments: true
|
||||||
php_errors:
|
php_errors:
|
||||||
log: true
|
log: true
|
||||||
|
|
||||||
|
form:
|
||||||
|
legacy_error_messages: false # Enable to use the new Form component validation messages
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
framework:
|
framework:
|
||||||
test: true
|
test: true
|
||||||
|
|
60
config/packages/hslavich_onelogin_saml.yaml
Normal file
60
config/packages/hslavich_onelogin_saml.yaml
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
# See https://github.com/SAML-Toolkits/php-saml for more information about the SAML settings
|
||||||
|
|
||||||
|
hslavich_onelogin_saml:
|
||||||
|
# Basic settings
|
||||||
|
idp:
|
||||||
|
entityId: '%env(string:SAML_IDP_ENTITY_ID)%'
|
||||||
|
singleSignOnService:
|
||||||
|
url: '%env(string:SAML_IDP_SINGLE_SIGN_ON_SERVICE)%'
|
||||||
|
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||||
|
singleLogoutService:
|
||||||
|
url: '%env(string:SAML_IDP_SINGLE_LOGOUT_SERVICE)%'
|
||||||
|
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||||
|
x509cert: '%env(string:SAML_IDP_X509_CERT)%'
|
||||||
|
sp:
|
||||||
|
entityId: '%env(string:SAML_SP_ENTITY_ID)%'
|
||||||
|
assertionConsumerService:
|
||||||
|
url: '%partdb.default_uri%saml/acs'
|
||||||
|
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
||||||
|
singleLogoutService:
|
||||||
|
url: '%partdb.default_uri%logout'
|
||||||
|
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||||
|
x509cert: '%env(string:SAML_SP_X509_CERT)%'
|
||||||
|
privateKey: '%env(string:SAMLP_SP_PRIVATE_KEY)%'
|
||||||
|
|
||||||
|
# Optional settings
|
||||||
|
#baseurl: 'http://myapp.com'
|
||||||
|
strict: true
|
||||||
|
debug: false
|
||||||
|
security:
|
||||||
|
allowRepeatAttributeName: true
|
||||||
|
# nameIdEncrypted: false
|
||||||
|
authnRequestsSigned: true
|
||||||
|
logoutRequestSigned: true
|
||||||
|
logoutResponseSigned: true
|
||||||
|
# wantMessagesSigned: false
|
||||||
|
# wantAssertionsSigned: true
|
||||||
|
# wantNameIdEncrypted: false
|
||||||
|
# requestedAuthnContext: true
|
||||||
|
# signMetadata: false
|
||||||
|
# wantXMLValidation: true
|
||||||
|
# relaxDestinationValidation: false
|
||||||
|
# destinationStrictlyMatches: true
|
||||||
|
# rejectUnsolicitedResponsesWithInResponseTo: false
|
||||||
|
# signatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
||||||
|
# digestAlgorithm: 'http://www.w3.org/2001/04/xmlenc#sha256'
|
||||||
|
#contactPerson:
|
||||||
|
# technical:
|
||||||
|
# givenName: 'Tech User'
|
||||||
|
# emailAddress: 'techuser@example.com'
|
||||||
|
# support:
|
||||||
|
# givenName: 'Support User'
|
||||||
|
# emailAddress: 'supportuser@example.com'
|
||||||
|
# administrative:
|
||||||
|
# givenName: 'Administrative User'
|
||||||
|
# emailAddress: 'administrativeuser@example.com'
|
||||||
|
#organization:
|
||||||
|
# en:
|
||||||
|
# name: 'Part-DB-name'
|
||||||
|
# displayname: 'Displayname'
|
||||||
|
# url: 'http://example.com'
|
|
@ -1,5 +0,0 @@
|
||||||
framework:
|
|
||||||
http_client:
|
|
||||||
default_options:
|
|
||||||
headers:
|
|
||||||
'User-Agent': 'Part-DB'
|
|
|
@ -1,10 +0,0 @@
|
||||||
services:
|
|
||||||
Psr\Http\Message\RequestFactoryInterface: '@http_discovery.psr17_factory'
|
|
||||||
Psr\Http\Message\ResponseFactoryInterface: '@http_discovery.psr17_factory'
|
|
||||||
Psr\Http\Message\ServerRequestFactoryInterface: '@http_discovery.psr17_factory'
|
|
||||||
Psr\Http\Message\StreamFactoryInterface: '@http_discovery.psr17_factory'
|
|
||||||
Psr\Http\Message\UploadedFileFactoryInterface: '@http_discovery.psr17_factory'
|
|
||||||
Psr\Http\Message\UriFactoryInterface: '@http_discovery.psr17_factory'
|
|
||||||
|
|
||||||
http_discovery.psr17_factory:
|
|
||||||
class: Http\Discovery\Psr17Factory
|
|
|
@ -1,38 +0,0 @@
|
||||||
knpu_oauth2_client:
|
|
||||||
clients:
|
|
||||||
# configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
|
|
||||||
|
|
||||||
ip_digikey_oauth:
|
|
||||||
type: generic
|
|
||||||
provider_class: '\League\OAuth2\Client\Provider\GenericProvider'
|
|
||||||
|
|
||||||
client_id: '%env(PROVIDER_DIGIKEY_CLIENT_ID)%'
|
|
||||||
client_secret: '%env(PROVIDER_DIGIKEY_SECRET)%'
|
|
||||||
|
|
||||||
redirect_route: 'oauth_client_check'
|
|
||||||
redirect_params: {name: 'ip_digikey_oauth'}
|
|
||||||
|
|
||||||
provider_options:
|
|
||||||
urlAuthorize: 'https://api.digikey.com/v1/oauth2/authorize'
|
|
||||||
urlAccessToken: 'https://api.digikey.com/v1/oauth2/token'
|
|
||||||
urlResourceOwnerDetails: ''
|
|
||||||
|
|
||||||
# Sandbox
|
|
||||||
#urlAuthorize: 'https://sandbox-api.digikey.com/v1/oauth2/authorize'
|
|
||||||
#urlAccessToken: 'https://sandbox-api.digikey.com/v1/oauth2/token'
|
|
||||||
#urlResourceOwnerDetails: ''
|
|
||||||
|
|
||||||
ip_octopart_oauth:
|
|
||||||
type: generic
|
|
||||||
provider_class: '\League\OAuth2\Client\Provider\GenericProvider'
|
|
||||||
|
|
||||||
client_id: '%env(PROVIDER_OCTOPART_CLIENT_ID)%'
|
|
||||||
client_secret: '%env(PROVIDER_OCTOPART_SECRET)%'
|
|
||||||
|
|
||||||
redirect_route: 'oauth_client_check'
|
|
||||||
redirect_params: { name: 'ip_octopart_oauth' }
|
|
||||||
|
|
||||||
provider_options:
|
|
||||||
urlAuthorize: 'https://identity.nexar.com/connect/authorize'
|
|
||||||
urlAccessToken: 'https://identity.nexar.com/connect/token'
|
|
||||||
urlResourceOwnerDetails: ''
|
|
2
config/packages/lock.yaml
Normal file
2
config/packages/lock.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
framework:
|
||||||
|
lock: '%env(LOCK_DSN)%'
|
|
@ -50,6 +50,7 @@ when@prod:
|
||||||
type: stream
|
type: stream
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
level: debug
|
level: debug
|
||||||
|
formatter: monolog.formatter.json
|
||||||
console:
|
console:
|
||||||
type: console
|
type: console
|
||||||
process_psr_3_messages: false
|
process_psr_3_messages: false
|
||||||
|
@ -73,6 +74,7 @@ when@docker:
|
||||||
type: stream
|
type: stream
|
||||||
path: "php://stderr"
|
path: "php://stderr"
|
||||||
level: debug
|
level: debug
|
||||||
|
formatter: monolog.formatter.json
|
||||||
console:
|
console:
|
||||||
type: console
|
type: console
|
||||||
process_psr_3_messages: false
|
process_psr_3_messages: false
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
# See https://github.com/SAML-Toolkits/php-saml for more information about the SAML settings
|
|
||||||
|
|
||||||
# Define a parameter here, so we can access it later in the default fallback
|
|
||||||
parameters:
|
|
||||||
saml.sp.privateKey: '%env(string:SAML_SP_PRIVATE_KEY)%'
|
|
||||||
|
|
||||||
nbgrp_onelogin_saml:
|
|
||||||
use_proxy_vars: '%env(bool:SAML_BEHIND_PROXY)%'
|
|
||||||
onelogin_settings:
|
|
||||||
default:
|
|
||||||
# Basic settings
|
|
||||||
idp:
|
|
||||||
entityId: '%env(string:SAML_IDP_ENTITY_ID)%'
|
|
||||||
singleSignOnService:
|
|
||||||
url: '%env(string:SAML_IDP_SINGLE_SIGN_ON_SERVICE)%'
|
|
||||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
|
||||||
singleLogoutService:
|
|
||||||
url: '%env(string:SAML_IDP_SINGLE_LOGOUT_SERVICE)%'
|
|
||||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
|
||||||
x509cert: '%env(string:SAML_IDP_X509_CERT)%'
|
|
||||||
sp:
|
|
||||||
entityId: '%env(string:SAML_SP_ENTITY_ID)%'
|
|
||||||
assertionConsumerService:
|
|
||||||
url: '%partdb.default_uri%saml/acs'
|
|
||||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
|
||||||
singleLogoutService:
|
|
||||||
url: '%partdb.default_uri%logout'
|
|
||||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
|
||||||
x509cert: '%env(string:SAML_SP_X509_CERT)%'
|
|
||||||
# Before the env variable was wrongly named "SAMLP_SP_PRIVATE_KEY".
|
|
||||||
# For compatibility reasons we keep it and only fallback to the new name if the old one is not set. This may be removed in the future.
|
|
||||||
privateKey: '%env(string:default:saml.sp.privateKey:string:SAMLP_SP_PRIVATE_KEY)%'
|
|
||||||
|
|
||||||
# Optional settings
|
|
||||||
baseurl: '%partdb.default_uri%saml/'
|
|
||||||
strict: true
|
|
||||||
debug: false
|
|
||||||
security:
|
|
||||||
allowRepeatAttributeName: true
|
|
||||||
# nameIdEncrypted: false
|
|
||||||
authnRequestsSigned: true
|
|
||||||
logoutRequestSigned: true
|
|
||||||
logoutResponseSigned: true
|
|
||||||
# wantMessagesSigned: false
|
|
||||||
# wantAssertionsSigned: true
|
|
||||||
# wantNameIdEncrypted: false
|
|
||||||
# requestedAuthnContext: true
|
|
||||||
# signMetadata: false
|
|
||||||
# wantXMLValidation: true
|
|
||||||
# relaxDestinationValidation: false
|
|
||||||
# destinationStrictlyMatches: true
|
|
||||||
# rejectUnsolicitedResponsesWithInResponseTo: false
|
|
||||||
# signatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
|
||||||
# digestAlgorithm: 'http://www.w3.org/2001/04/xmlenc#sha256'
|
|
||||||
#contactPerson:
|
|
||||||
# technical:
|
|
||||||
# givenName: 'Tech User'
|
|
||||||
# emailAddress: 'techuser@example.com'
|
|
||||||
# support:
|
|
||||||
# givenName: 'Support User'
|
|
||||||
# emailAddress: 'supportuser@example.com'
|
|
||||||
# administrative:
|
|
||||||
# givenName: 'Administrative User'
|
|
||||||
# emailAddress: 'administrativeuser@example.com'
|
|
||||||
#organization:
|
|
||||||
# en:
|
|
||||||
# name: 'Part-DB-name'
|
|
||||||
# displayname: 'Displayname'
|
|
||||||
# url: 'http://example.com'
|
|
|
@ -1,10 +0,0 @@
|
||||||
nelmio_cors:
|
|
||||||
defaults:
|
|
||||||
origin_regex: true
|
|
||||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
|
||||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
|
||||||
allow_headers: ['Content-Type', 'Authorization']
|
|
||||||
expose_headers: ['Link']
|
|
||||||
max_age: 3600
|
|
||||||
paths:
|
|
||||||
'^/': null
|
|
|
@ -12,13 +12,6 @@ nelmio_security:
|
||||||
external_redirects:
|
external_redirects:
|
||||||
abort: true
|
abort: true
|
||||||
log: true
|
log: true
|
||||||
allow_list:
|
|
||||||
# Whitelist the domain of the SAML IDP, so we can redirect to it during the SAML login process
|
|
||||||
- '%env(string:key:host:url:SAML_IDP_SINGLE_SIGN_ON_SERVICE)%'
|
|
||||||
|
|
||||||
# Whitelist the info provider APIs (OAuth redirects)
|
|
||||||
- 'digikey.com'
|
|
||||||
- 'nexar.com'
|
|
||||||
|
|
||||||
# forces Microsoft's XSS-Protection with
|
# forces Microsoft's XSS-Protection with
|
||||||
# its block mode
|
# its block mode
|
||||||
|
@ -51,16 +44,12 @@ nelmio_security:
|
||||||
img-src:
|
img-src:
|
||||||
- '*'
|
- '*'
|
||||||
- 'data:'
|
- 'data:'
|
||||||
# Required for be able to load pictures in the QR code scanner
|
|
||||||
- 'blob:'
|
|
||||||
style-src:
|
style-src:
|
||||||
- 'self'
|
- 'self'
|
||||||
- 'unsafe-inline'
|
- 'unsafe-inline'
|
||||||
- 'data:'
|
- 'data:'
|
||||||
script-src:
|
script-src:
|
||||||
- 'self'
|
- 'self'
|
||||||
# Required for loading the Wasm for the barcode scanner:
|
|
||||||
- 'wasm-unsafe-eval'
|
|
||||||
object-src:
|
object-src:
|
||||||
- 'self'
|
- 'self'
|
||||||
- 'data:'
|
- 'data:'
|
||||||
|
|
11
config/packages/php_translation.yaml
Normal file
11
config/packages/php_translation.yaml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
translation:
|
||||||
|
locales: ["en", "de"]
|
||||||
|
edit_in_place:
|
||||||
|
enabled: false
|
||||||
|
config_name: app
|
||||||
|
configs:
|
||||||
|
app:
|
||||||
|
dirs: ["%kernel.project_dir%/templates", "%kernel.project_dir%/src"]
|
||||||
|
output_dir: "%kernel.project_dir%/translations"
|
||||||
|
excluded_names: ["*TestCase.php", "*Test.php"]
|
||||||
|
excluded_dirs: [cache, data, logs]
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue