Compare commits

..

No commits in common. "master" and "v1.0.0-beta.1" have entirely different histories.

1289 changed files with 28925 additions and 256171 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -1,2 +0,0 @@
opcache.preload_user = root
opcache.preload = /app/config/preload.php

View file

@ -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 "$@"

View file

@ -1,4 +0,0 @@
worker {
file ./public/index.php
env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
}

View file

@ -39,51 +39,6 @@ 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)
php-fpmPHP_VERSION -F &
# 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
set -- apache2-foreground "$@" set -- apache2-foreground "$@"

View file

@ -24,6 +24,16 @@
ErrorLog ${APACHE_LOG_DIR}/error.log ErrorLog ${APACHE_LOG_DIR}/error.log
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)
PassEnv APP_ENV APP_DEBUG APP_SECRET
PassEnv DATABASE_URL
PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR
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
PassEnv ERROR_PAGE_ADMIN_EMAIL ERROR_PAGE_SHOW_HELP
PassEnv DEMO_MODE NO_URL_REWRITE_AVAILABLE FIXER_API_KEY BANNER
# 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
# include a line for only one particular virtual host. For example the # include a line for only one particular virtual host. For example the

View file

@ -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

View file

@ -1,17 +0,0 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{compose.yaml,compose.*.yaml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false

114
.env
View file

@ -1,5 +1,5 @@
#### Part-DB Configuration #### Part-DB Configuration
# See https://docs.part-db.de/configuration.html for documentation of available options # See https://github.com/Part-DB/Part-DB-symfony/wiki/Configuration for documentation of available options
################################################################################### ###################################################################################
# Database settings # Database settings
@ -14,25 +14,23 @@ 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
################################################################################### ###################################################################################
# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates or when no request context is available. # The language to use serverwide as default (en, de, ru, etc.)
DEFAULT_URI="https://partdb.changeme.invalid/" DEFAULT_LANG="en"
# The default timezone to use serverwide (e.g. Europe/Berlin)
DEFAULT_TIMEZONE="Europe/Berlin"
# The currency that is used inside the DB (and is assumed when no currency is set). This can not be changed later, so be sure to set it the currency used in your country
BASE_CURRENCY="EUR"
# The name of this installation. This will be shown as title in the browser and in the header of the website
INSTANCE_NAME="Part-DB"
# 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)
ALLOW_ATTACHMENT_DOWNLOADS=0
# Use gravatars for user avatars, when user has no own avatar defined
USE_GRAVATAR=0
################################################################################### ###################################################################################
# Email settings # Email settings
@ -50,6 +48,18 @@ EMAIL_SENDER_NAME="Part-DB Mailer"
# Set this to 1 to allow reset of a password per email # Set this to 1 to allow reset of a password per email
ALLOW_EMAIL_PW_RESET=0 ALLOW_EMAIL_PW_RESET=0
######################################################################################
# History/Eventlog settings
######################################################################################
# If you want to use full timetrave functionality all values below have to be set to 1
# Save which fields were changed in a ElementEdited log entry
HISTORY_SAVE_CHANGED_FIELDS=1
# Save the old data in the ElementEdited log entry (warning this could increase the database size in short time)
HISTORY_SAVE_CHANGED_DATA=1
# Save the data of an element that gets removed into log entry. This allows to undelete an element
HISTORY_SAVE_REMOVED_DATA=1
################################################################################### ###################################################################################
# Error pages settings # Error pages settings
################################################################################### ###################################################################################
@ -59,46 +69,6 @@ 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
###################################################################################
# SAML Single sign on-settings
###################################################################################
# Set this to 1 to enable SAML single sign on
# Be also sure to set the correct values for DEFAULT_URI
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 }
# 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
SAML_ROLE_MAPPING='{}'
# A mapping could look like the following
#SAML_ROLE_MAPPING='{ "*": 2, "admin": 1, "editor": 3}'
# When this is set to 1, the group of SAML users will be updated everytime they login based on their SAML roles
SAML_UPDATE_GROUP_ON_LOGIN=1
# The entity ID of your SAML IDP (e.g. the realm name of your Keycloak server)
SAML_IDP_ENTITY_ID="https://idp.changeme.invalid/realms/master"
# The URL of your SAML IDP SingleSignOnService (e.g. the endpoint of your Keycloak server)
SAML_IDP_SINGLE_SIGN_ON_SERVICE="https://idp.changeme.invalid/realms/master/protocol/saml"
# The URL of your SAML IDP SingleLogoutService (e.g. the endpoint of your Keycloak server)
SAML_IDP_SINGLE_LOGOUT_SERVICE="https://idp.changeme.invalid/realms/master/protocol/saml"
# The public certificate of the SAML IDP (e.g. the certificate of your Keycloak server)
SAML_IDP_X509_CERT="MIIC..."
# The entity of your SAML SP, must match the SP entityID configured in your SAML IDP (e.g. Keycloak).
# This should be a the domain name of your Part-DB installation, followed by "/sp"
SAML_SP_ENTITY_ID="https://partdb.changeme.invalid/sp"
# The public certificate of the SAML SP
SAML_SP_X509_CERT="MIIC..."
# The private key of the SAML SP
SAML_SP_PRIVATE_KEY="MIIE..."
###################################################################################### ######################################################################################
# Other settings # Other settings
###################################################################################### ######################################################################################
@ -109,29 +79,17 @@ 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. # If you want to use fixer.io for currency conversion, you have to set this to your API key
REDIRECT_TO_HTTPS=0 FIXER_API_KEY=CHANGEME
# 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) # Override value if you want to show to show a given text on homepage.
DISABLE_YEAR2038_BUG_CHECK=0 # When this is empty the content of config/banner.md is used as banner
BANNER=""
# 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_HOSTS='^(localhost|example\.com)$'
###> symfony/lock ###
# Choose one of the stores below
# postgresql+advisory://db_user:db_password@localhost/db_name
LOCK_DSN=flock
###< symfony/lock ###
###> nelmio/cors-bundle ###
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
###< nelmio/cors-bundle ###
###> symfony/framework-bundle ###
APP_ENV=prod APP_ENV=prod
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849 APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
APP_SHARE_DIR=var/share
###< symfony/framework-bundle ###
# Set the trusted IPs here, when using an reverse proxy
#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)$'

View file

@ -1,4 +0,0 @@
###> symfony/framework-bundle ###
APP_SECRET=318b5d659e07a0b3f96d9b3a83b254ca
###< symfony/framework-bundle ###

View file

@ -5,11 +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
INSTANCE_NAME="Part-DB"

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
# For sh files, always use LF line endings
*.sh text eol=lf

View file

@ -1,752 +0,0 @@
-- phpMyAdmin SQL Dump
-- version 5.1.3
-- https://www.phpmyadmin.net/
--
-- Host: 127.0.0.1
-- Erstellungszeit: 07. Mai 2023 um 01:58
-- Server-Version: 10.6.5-MariaDB-log
-- PHP-Version: 8.1.2
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Datenbank: `partdb_demo`
--
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `attachements`
--
CREATE TABLE `attachements` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`class_name` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL,
`element_id` int(11) NOT NULL,
`type_id` int(11) NOT NULL,
`filename` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`show_in_table` tinyint(1) NOT NULL DEFAULT 0,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `attachements`
--
INSERT INTO `attachements` (`id`, `name`, `class_name`, `element_id`, `type_id`, `filename`, `show_in_table`, `last_modified`) VALUES
(1, 'BC547', 'Part', 2, 2, '%BASE%/data/media/bc547.pdf', 1, '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `attachement_types`
--
CREATE TABLE `attachement_types` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `attachement_types`
--
INSERT INTO `attachement_types` (`id`, `name`, `parent_id`, `comment`, `datetime_added`, `last_modified`) VALUES
(1, 'Bilder', NULL, NULL, '2017-10-21 17:58:48', '0000-00-00 00:00:00'),
(2, 'Datenblätter', NULL, NULL, '2017-10-21 17:58:48', '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `categories`
--
CREATE TABLE `categories` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`disable_footprints` tinyint(1) NOT NULL DEFAULT 0,
`disable_manufacturers` tinyint(1) NOT NULL DEFAULT 0,
`disable_autodatasheets` tinyint(1) NOT NULL DEFAULT 0,
`disable_properties` tinyint(1) NOT NULL DEFAULT 0,
`partname_regex` text COLLATE utf8mb3_unicode_ci NOT NULL,
`partname_hint` text COLLATE utf8mb3_unicode_ci NOT NULL,
`default_description` text COLLATE utf8mb3_unicode_ci NOT NULL,
`default_comment` text COLLATE utf8mb3_unicode_ci NOT NULL,
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `categories`
--
INSERT INTO `categories` (`id`, `name`, `parent_id`, `disable_footprints`, `disable_manufacturers`, `disable_autodatasheets`, `disable_properties`, `partname_regex`, `partname_hint`, `default_description`, `default_comment`, `comment`, `datetime_added`, `last_modified`) VALUES
(1, 'aktive Bauteile', NULL, 0, 0, 0, 0, '', '', '', '', NULL, '2017-10-21 17:58:49', '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `devices`
--
CREATE TABLE `devices` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`order_quantity` int(11) NOT NULL DEFAULT 0,
`order_only_missing_parts` tinyint(1) NOT NULL DEFAULT 0,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `devices`
--
INSERT INTO `devices` (`id`, `name`, `parent_id`, `order_quantity`, `order_only_missing_parts`, `datetime_added`, `last_modified`, `comment`) VALUES
(1, 'Test', NULL, 0, 0, '2015-04-16 15:08:56', '0000-00-00 00:00:00', NULL);
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `device_parts`
--
CREATE TABLE `device_parts` (
`id` int(11) NOT NULL,
`id_part` int(11) NOT NULL DEFAULT 0,
`id_device` int(11) NOT NULL DEFAULT 0,
`quantity` int(11) NOT NULL DEFAULT 0,
`mountnames` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `device_parts`
--
INSERT INTO `device_parts` (`id`, `id_part`, `id_device`, `quantity`, `mountnames`) VALUES
(1, 2, 1, 1, '');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `footprints`
--
CREATE TABLE `footprints` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`filename` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`filename_3d` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `footprints`
--
INSERT INTO `footprints` (`id`, `name`, `filename`, `filename_3d`, `parent_id`, `comment`, `datetime_added`, `last_modified`) VALUES
(1, 'LEDs', '%BASE%/img/footprints/Optik/LEDs/Bedrahtet/LED-GELB_3MM.png', '', NULL, NULL, '2017-10-21 17:58:49', '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `groups`
--
CREATE TABLE `groups` (
`id` int(11) NOT NULL,
`name` varchar(32) NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` mediumtext DEFAULT NULL,
`perms_system` int(11) NOT NULL,
`perms_groups` int(11) NOT NULL,
`perms_users` int(11) NOT NULL,
`perms_self` int(11) NOT NULL,
`perms_system_config` int(11) NOT NULL,
`perms_system_database` int(11) NOT NULL,
`perms_parts` bigint(11) NOT NULL,
`perms_parts_name` smallint(6) NOT NULL,
`perms_parts_description` smallint(6) NOT NULL,
`perms_parts_instock` smallint(6) NOT NULL,
`perms_parts_mininstock` smallint(6) NOT NULL,
`perms_parts_footprint` smallint(6) NOT NULL,
`perms_parts_storelocation` smallint(6) NOT NULL,
`perms_parts_manufacturer` smallint(6) NOT NULL,
`perms_parts_comment` smallint(6) NOT NULL,
`perms_parts_order` smallint(6) NOT NULL,
`perms_parts_orderdetails` smallint(6) NOT NULL,
`perms_parts_prices` smallint(6) NOT NULL,
`perms_parts_attachements` smallint(6) NOT NULL,
`perms_devices` int(11) NOT NULL,
`perms_devices_parts` int(11) NOT NULL,
`perms_storelocations` int(11) NOT NULL,
`perms_footprints` int(11) NOT NULL,
`perms_categories` int(11) NOT NULL,
`perms_suppliers` int(11) NOT NULL,
`perms_manufacturers` int(11) NOT NULL,
`perms_attachement_types` int(11) NOT NULL,
`perms_tools` int(11) NOT NULL,
`perms_labels` smallint(6) NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
--
-- Daten für Tabelle `groups`
--
INSERT INTO `groups` (`id`, `name`, `parent_id`, `comment`, `perms_system`, `perms_groups`, `perms_users`, `perms_self`, `perms_system_config`, `perms_system_database`, `perms_parts`, `perms_parts_name`, `perms_parts_description`, `perms_parts_instock`, `perms_parts_mininstock`, `perms_parts_footprint`, `perms_parts_storelocation`, `perms_parts_manufacturer`, `perms_parts_comment`, `perms_parts_order`, `perms_parts_orderdetails`, `perms_parts_prices`, `perms_parts_attachements`, `perms_devices`, `perms_devices_parts`, `perms_storelocations`, `perms_footprints`, `perms_categories`, `perms_suppliers`, `perms_manufacturers`, `perms_attachement_types`, `perms_tools`, `perms_labels`, `datetime_added`, `last_modified`) VALUES
(1, 'admins', NULL, 'Users of this group can do everything: Read, Write and Administrative actions.', 21, 1365, 87381, 85, 85, 21, 1431655765, 5, 5, 5, 5, 5, 5, 5, 5, 5, 325, 325, 325, 5461, 325, 5461, 5461, 5461, 5461, 5461, 1365, 1365, 85, '2017-10-21 17:58:46', '2018-10-08 17:27:41'),
(2, 'readonly', NULL, 'Users of this group can only read informations, use tools, and don\'t have access to administrative tools.', 2, 2730, 43690, 25, 170, 42, 2778027689, 9, 9, 9, 9, 9, 9, 9, 9, 9, 649, 649, 649, 1705, 649, 1705, 1705, 1705, 1705, 1705, 681, 1366, 165, '2017-10-21 17:58:46', '2018-10-08 17:28:35'),
(3, 'users', NULL, 'Users of this group, can edit part informations, create new ones, etc. but are not allowed to use administrative tools. (But can read current configuration, and see Server status)', 42, 2730, 43689, 89, 105, 41, 1431655765, 5, 5, 5, 5, 5, 5, 5, 5, 5, 325, 325, 325, 5461, 325, 5461, 5461, 5461, 5461, 5461, 1365, 1365, 85, '2017-10-21 17:58:46', '2018-10-08 17:28:17');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `internal`
--
CREATE TABLE `internal` (
`keyName` char(30) CHARACTER SET ascii NOT NULL,
`keyValue` varchar(255) COLLATE utf8mb3_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `internal`
--
INSERT INTO `internal` (`keyName`, `keyValue`) VALUES
('dbVersion', '26');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `log`
--
CREATE TABLE `log` (
`id` int(11) NOT NULL,
`datetime` timestamp NOT NULL DEFAULT current_timestamp(),
`id_user` int(11) NOT NULL,
`level` tinyint(4) NOT NULL,
`type` smallint(6) NOT NULL,
`target_id` int(11) NOT NULL,
`target_type` smallint(6) NOT NULL,
`extra` mediumtext NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `manufacturers`
--
CREATE TABLE `manufacturers` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`address` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`phone_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`fax_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`email_address` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`website` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`auto_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `manufacturers`
--
INSERT INTO `manufacturers` (`id`, `name`, `parent_id`, `address`, `phone_number`, `fax_number`, `email_address`, `website`, `auto_product_url`, `datetime_added`, `comment`, `last_modified`) VALUES
(1, 'Atmel', NULL, '', '', '', '', '', '', '2015-03-01 11:27:10', NULL, '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `orderdetails`
--
CREATE TABLE `orderdetails` (
`id` int(11) NOT NULL,
`part_id` int(11) NOT NULL,
`id_supplier` int(11) NOT NULL DEFAULT 0,
`supplierpartnr` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`obsolete` tinyint(1) DEFAULT 0,
`supplier_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `parts`
--
CREATE TABLE `parts` (
`id` int(11) NOT NULL,
`id_category` int(11) NOT NULL DEFAULT 0,
`name` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`description` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`instock` int(11) NOT NULL DEFAULT 0,
`mininstock` int(11) NOT NULL DEFAULT 0,
`comment` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`visible` tinyint(1) NOT NULL,
`id_footprint` int(11) DEFAULT NULL,
`id_storelocation` int(11) DEFAULT NULL,
`order_orderdetails_id` int(11) DEFAULT NULL,
`order_quantity` int(11) NOT NULL DEFAULT 1,
`manual_order` tinyint(1) NOT NULL DEFAULT 0,
`id_manufacturer` int(11) DEFAULT NULL,
`id_master_picture_attachement` int(11) DEFAULT NULL,
`manufacturer_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`favorite` tinyint(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `parts`
--
INSERT INTO `parts` (`id`, `id_category`, `name`, `description`, `instock`, `mininstock`, `comment`, `visible`, `id_footprint`, `id_storelocation`, `order_orderdetails_id`, `order_quantity`, `manual_order`, `id_manufacturer`, `id_master_picture_attachement`, `manufacturer_product_url`, `datetime_added`, `last_modified`, `favorite`) VALUES
(2, 1, 'BC547C', 'NPN 45V 0,1A 0,5W', 59, 0, '', 0, 1, 1, NULL, 1, 0, NULL, NULL, '', '2015-03-01 10:40:31', '2016-12-26 10:48:49', 0);
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `pricedetails`
--
CREATE TABLE `pricedetails` (
`id` int(11) NOT NULL,
`orderdetails_id` int(11) NOT NULL,
`price` decimal(11,5) DEFAULT NULL,
`price_related_quantity` int(11) NOT NULL DEFAULT 1,
`min_discount_quantity` int(11) NOT NULL DEFAULT 1,
`manual_input` tinyint(1) NOT NULL DEFAULT 1,
`last_modified` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `storelocations`
--
CREATE TABLE `storelocations` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`is_full` tinyint(1) NOT NULL DEFAULT 0,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `storelocations`
--
INSERT INTO `storelocations` (`id`, `name`, `parent_id`, `is_full`, `datetime_added`, `comment`, `last_modified`) VALUES
(1, 'Halbleiter I', NULL, 0, '2015-03-01 11:26:37', NULL, '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `suppliers`
--
CREATE TABLE `suppliers` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`address` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`phone_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`fax_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`email_address` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`website` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`auto_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `suppliers`
--
INSERT INTO `suppliers` (`id`, `name`, `parent_id`, `address`, `phone_number`, `fax_number`, `email_address`, `website`, `auto_product_url`, `datetime_added`, `comment`, `last_modified`) VALUES
(1, 'Test', NULL, '', '', '', '', '', 'Test', '2015-03-01 10:37:23', NULL, '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `users`
--
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`name` varchar(32) NOT NULL,
`password` varchar(255) DEFAULT NULL,
`first_name` tinytext DEFAULT NULL,
`last_name` tinytext DEFAULT NULL,
`department` tinytext DEFAULT NULL,
`email` tinytext DEFAULT NULL,
`need_pw_change` tinyint(1) NOT NULL DEFAULT 0,
`group_id` int(11) DEFAULT NULL,
`config_language` tinytext DEFAULT NULL,
`config_timezone` tinytext DEFAULT NULL,
`config_theme` tinytext DEFAULT NULL,
`config_currency` tinytext DEFAULT NULL,
`config_image_path` text NOT NULL,
`config_instock_comment_w` text NOT NULL,
`config_instock_comment_a` text NOT NULL,
`perms_system` int(11) NOT NULL,
`perms_groups` int(11) NOT NULL,
`perms_users` int(11) NOT NULL,
`perms_self` int(11) NOT NULL,
`perms_system_config` int(11) NOT NULL,
`perms_system_database` int(11) NOT NULL,
`perms_parts` bigint(11) NOT NULL,
`perms_parts_name` smallint(6) NOT NULL,
`perms_parts_description` smallint(6) NOT NULL,
`perms_parts_instock` smallint(6) NOT NULL,
`perms_parts_mininstock` smallint(6) NOT NULL,
`perms_parts_footprint` smallint(6) NOT NULL,
`perms_parts_storelocation` smallint(6) NOT NULL,
`perms_parts_manufacturer` smallint(6) NOT NULL,
`perms_parts_comment` smallint(6) NOT NULL,
`perms_parts_order` smallint(6) NOT NULL,
`perms_parts_orderdetails` smallint(6) NOT NULL,
`perms_parts_prices` smallint(6) NOT NULL,
`perms_parts_attachements` smallint(6) NOT NULL,
`perms_devices` int(11) NOT NULL,
`perms_devices_parts` int(11) NOT NULL,
`perms_storelocations` int(11) NOT NULL,
`perms_footprints` int(11) NOT NULL,
`perms_categories` int(11) NOT NULL,
`perms_suppliers` int(11) NOT NULL,
`perms_manufacturers` int(11) NOT NULL,
`perms_attachement_types` int(11) NOT NULL,
`perms_tools` int(11) NOT NULL,
`perms_labels` smallint(6) NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
--
-- Daten für Tabelle `users`
--
INSERT INTO `users` (`id`, `name`, `password`, `first_name`, `last_name`, `department`, `email`, `need_pw_change`, `group_id`, `config_language`, `config_timezone`, `config_theme`, `config_currency`, `config_image_path`, `config_instock_comment_w`, `config_instock_comment_a`, `perms_system`, `perms_groups`, `perms_users`, `perms_self`, `perms_system_config`, `perms_system_database`, `perms_parts`, `perms_parts_name`, `perms_parts_description`, `perms_parts_instock`, `perms_parts_mininstock`, `perms_parts_footprint`, `perms_parts_storelocation`, `perms_parts_manufacturer`, `perms_parts_comment`, `perms_parts_order`, `perms_parts_orderdetails`, `perms_parts_prices`, `perms_parts_attachements`, `perms_devices`, `perms_devices_parts`, `perms_storelocations`, `perms_footprints`, `perms_categories`, `perms_suppliers`, `perms_manufacturers`, `perms_attachement_types`, `perms_tools`, `perms_labels`, `datetime_added`, `last_modified`) VALUES
(1, 'anonymous', '', '', '', '', '', 0, 2, '', '', '', NULL, '', '', '', 21848, 20480, 0, 0, 0, 0, 0, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21520, 21520, 21520, 20480, 21520, 20480, 20480, 20480, 20480, 20480, 21504, 20480, 0, '2017-10-21 17:58:46', '2018-02-18 12:46:58'),
(2, 'admin', '$2a$12$j0RKrKlx60bzX1DWMyXwjeaW.pe3bFjAK8ByIGnvjrRnET2JtsFoe', 'Admin', 'Ad', NULL, 'admin@ras.pi', 0, 1, '', '', '', NULL, '', '', '', 21845, 21845, 21845, 21, 85, 21, 349525, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 0, '2017-10-21 17:58:46', '2017-12-23 11:04:48');
--
-- Indizes der exportierten Tabellen
--
--
-- Indizes für die Tabelle `attachements`
--
ALTER TABLE `attachements`
ADD PRIMARY KEY (`id`),
ADD KEY `attachements_class_name_k` (`class_name`),
ADD KEY `attachements_element_id_k` (`element_id`),
ADD KEY `attachements_type_id_fk` (`type_id`);
--
-- Indizes für die Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
ADD PRIMARY KEY (`id`),
ADD KEY `attachement_types_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `categories`
--
ALTER TABLE `categories`
ADD PRIMARY KEY (`id`),
ADD KEY `categories_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `devices`
--
ALTER TABLE `devices`
ADD PRIMARY KEY (`id`),
ADD KEY `devices_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `device_parts`
--
ALTER TABLE `device_parts`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `device_parts_combination_uk` (`id_part`,`id_device`),
ADD KEY `device_parts_id_part_k` (`id_part`),
ADD KEY `device_parts_id_device_k` (`id_device`);
--
-- Indizes für die Tabelle `footprints`
--
ALTER TABLE `footprints`
ADD PRIMARY KEY (`id`),
ADD KEY `footprints_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `groups`
--
ALTER TABLE `groups`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- Indizes für die Tabelle `internal`
--
ALTER TABLE `internal`
ADD UNIQUE KEY `keyName` (`keyName`);
--
-- Indizes für die Tabelle `log`
--
ALTER TABLE `log`
ADD PRIMARY KEY (`id`),
ADD KEY `id_user` (`id_user`);
--
-- Indizes für die Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
ADD PRIMARY KEY (`id`),
ADD KEY `manufacturers_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `orderdetails`
--
ALTER TABLE `orderdetails`
ADD PRIMARY KEY (`id`),
ADD KEY `orderdetails_part_id_k` (`part_id`),
ADD KEY `orderdetails_id_supplier_k` (`id_supplier`);
--
-- Indizes für die Tabelle `parts`
--
ALTER TABLE `parts`
ADD PRIMARY KEY (`id`),
ADD KEY `parts_id_category_k` (`id_category`),
ADD KEY `parts_id_footprint_k` (`id_footprint`),
ADD KEY `parts_id_storelocation_k` (`id_storelocation`),
ADD KEY `parts_order_orderdetails_id_k` (`order_orderdetails_id`),
ADD KEY `parts_id_manufacturer_k` (`id_manufacturer`),
ADD KEY `favorite` (`favorite`);
--
-- Indizes für die Tabelle `pricedetails`
--
ALTER TABLE `pricedetails`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `pricedetails_combination_uk` (`orderdetails_id`,`min_discount_quantity`),
ADD KEY `pricedetails_orderdetails_id_k` (`orderdetails_id`);
--
-- Indizes für die Tabelle `storelocations`
--
ALTER TABLE `storelocations`
ADD PRIMARY KEY (`id`),
ADD KEY `storelocations_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `suppliers`
--
ALTER TABLE `suppliers`
ADD PRIMARY KEY (`id`),
ADD KEY `suppliers_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- AUTO_INCREMENT für exportierte Tabellen
--
--
-- AUTO_INCREMENT für Tabelle `attachements`
--
ALTER TABLE `attachements`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=201;
--
-- AUTO_INCREMENT für Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- AUTO_INCREMENT für Tabelle `categories`
--
ALTER TABLE `categories`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=123;
--
-- AUTO_INCREMENT für Tabelle `devices`
--
ALTER TABLE `devices`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `device_parts`
--
ALTER TABLE `device_parts`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=12;
--
-- AUTO_INCREMENT für Tabelle `footprints`
--
ALTER TABLE `footprints`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=82;
--
-- AUTO_INCREMENT für Tabelle `groups`
--
ALTER TABLE `groups`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- AUTO_INCREMENT für Tabelle `log`
--
ALTER TABLE `log`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=218;
--
-- AUTO_INCREMENT für Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `orderdetails`
--
ALTER TABLE `orderdetails`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=650;
--
-- AUTO_INCREMENT für Tabelle `parts`
--
ALTER TABLE `parts`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1171;
--
-- AUTO_INCREMENT für Tabelle `pricedetails`
--
ALTER TABLE `pricedetails`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=437;
--
-- AUTO_INCREMENT für Tabelle `storelocations`
--
ALTER TABLE `storelocations`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=340;
--
-- AUTO_INCREMENT für Tabelle `suppliers`
--
ALTER TABLE `suppliers`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
--
-- AUTO_INCREMENT für Tabelle `users`
--
ALTER TABLE `users`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- Constraints der exportierten Tabellen
--
--
-- Constraints der Tabelle `attachements`
--
ALTER TABLE `attachements`
ADD CONSTRAINT `attachements_type_id_fk` FOREIGN KEY (`type_id`) REFERENCES `attachement_types` (`id`);
--
-- Constraints der Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
ADD CONSTRAINT `attachement_types_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `attachement_types` (`id`);
--
-- Constraints der Tabelle `categories`
--
ALTER TABLE `categories`
ADD CONSTRAINT `categories_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `categories` (`id`);
--
-- Constraints der Tabelle `devices`
--
ALTER TABLE `devices`
ADD CONSTRAINT `devices_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `devices` (`id`);
--
-- Constraints der Tabelle `footprints`
--
ALTER TABLE `footprints`
ADD CONSTRAINT `footprints_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `footprints` (`id`);
--
-- Constraints der Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
ADD CONSTRAINT `manufacturers_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `manufacturers` (`id`);
--
-- Constraints der Tabelle `parts`
--
ALTER TABLE `parts`
ADD CONSTRAINT `parts_id_footprint_fk` FOREIGN KEY (`id_footprint`) REFERENCES `footprints` (`id`),
ADD CONSTRAINT `parts_id_manufacturer_fk` FOREIGN KEY (`id_manufacturer`) REFERENCES `manufacturers` (`id`),
ADD CONSTRAINT `parts_id_storelocation_fk` FOREIGN KEY (`id_storelocation`) REFERENCES `storelocations` (`id`),
ADD CONSTRAINT `parts_order_orderdetails_id_fk` FOREIGN KEY (`order_orderdetails_id`) REFERENCES `orderdetails` (`id`);
--
-- Constraints der Tabelle `storelocations`
--
ALTER TABLE `storelocations`
ADD CONSTRAINT `storelocations_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `storelocations` (`id`);
--
-- Constraints der Tabelle `suppliers`
--
ALTER TABLE `suppliers`
ADD CONSTRAINT `suppliers_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `suppliers` (`id`);
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View file

@ -1,736 +0,0 @@
-- phpMyAdmin SQL Dump
-- version 5.1.3
-- https://www.phpmyadmin.net/
--
-- Host: 127.0.0.1
-- Erstellungszeit: 07. Mai 2023 um 01:48
-- Server-Version: 10.6.5-MariaDB-log
-- PHP-Version: 8.1.2
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Datenbank: `partdb-legacy`
--
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `attachements`
--
CREATE TABLE `attachements` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`class_name` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL,
`element_id` int(11) NOT NULL,
`type_id` int(11) NOT NULL,
`filename` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`show_in_table` tinyint(1) NOT NULL DEFAULT 0,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `attachement_types`
--
CREATE TABLE `attachement_types` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `attachement_types`
--
INSERT INTO `attachement_types` (`id`, `name`, `parent_id`, `comment`, `datetime_added`, `last_modified`) VALUES
(1, 'Bilder', NULL, NULL, '2023-01-07 18:31:48', '0000-00-00 00:00:00'),
(2, 'Datenblätter', NULL, NULL, '2023-01-07 18:31:48', '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `categories`
--
CREATE TABLE `categories` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`disable_footprints` tinyint(1) NOT NULL DEFAULT 0,
`disable_manufacturers` tinyint(1) NOT NULL DEFAULT 0,
`disable_autodatasheets` tinyint(1) NOT NULL DEFAULT 0,
`disable_properties` tinyint(1) NOT NULL DEFAULT 0,
`partname_regex` text COLLATE utf8mb3_unicode_ci NOT NULL DEFAULT '',
`partname_hint` text COLLATE utf8mb3_unicode_ci NOT NULL DEFAULT '',
`default_description` text COLLATE utf8mb3_unicode_ci NOT NULL DEFAULT '',
`default_comment` text COLLATE utf8mb3_unicode_ci NOT NULL DEFAULT '',
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `categories`
--
INSERT INTO `categories` (`id`, `name`, `parent_id`, `disable_footprints`, `disable_manufacturers`, `disable_autodatasheets`, `disable_properties`, `partname_regex`, `partname_hint`, `default_description`, `default_comment`, `comment`, `datetime_added`, `last_modified`) VALUES
(1, 'Test', NULL, 0, 0, 0, 0, '', '', '', '', '', '2023-01-07 18:32:29', '2023-01-07 18:32:29');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `devices`
--
CREATE TABLE `devices` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`order_quantity` int(11) NOT NULL DEFAULT 0,
`order_only_missing_parts` tinyint(1) NOT NULL DEFAULT 0,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `device_parts`
--
CREATE TABLE `device_parts` (
`id` int(11) NOT NULL,
`id_part` int(11) NOT NULL DEFAULT 0,
`id_device` int(11) NOT NULL DEFAULT 0,
`quantity` int(11) NOT NULL DEFAULT 0,
`mountnames` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `footprints`
--
CREATE TABLE `footprints` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`filename` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`filename_3d` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `groups`
--
CREATE TABLE `groups` (
`id` int(11) NOT NULL,
`name` varchar(32) NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` mediumtext DEFAULT NULL,
`perms_system` int(11) NOT NULL,
`perms_groups` int(11) NOT NULL,
`perms_users` int(11) NOT NULL,
`perms_self` int(11) NOT NULL,
`perms_system_config` int(11) NOT NULL,
`perms_system_database` int(11) NOT NULL,
`perms_parts` bigint(11) NOT NULL,
`perms_parts_name` smallint(6) NOT NULL,
`perms_parts_description` smallint(6) NOT NULL,
`perms_parts_instock` smallint(6) NOT NULL,
`perms_parts_mininstock` smallint(6) NOT NULL,
`perms_parts_footprint` smallint(6) NOT NULL,
`perms_parts_storelocation` smallint(6) NOT NULL,
`perms_parts_manufacturer` smallint(6) NOT NULL,
`perms_parts_comment` smallint(6) NOT NULL,
`perms_parts_order` smallint(6) NOT NULL,
`perms_parts_orderdetails` smallint(6) NOT NULL,
`perms_parts_prices` smallint(6) NOT NULL,
`perms_parts_attachements` smallint(6) NOT NULL,
`perms_devices` int(11) NOT NULL,
`perms_devices_parts` int(11) NOT NULL,
`perms_storelocations` int(11) NOT NULL,
`perms_footprints` int(11) NOT NULL,
`perms_categories` int(11) NOT NULL,
`perms_suppliers` int(11) NOT NULL,
`perms_manufacturers` int(11) NOT NULL,
`perms_attachement_types` int(11) NOT NULL,
`perms_tools` int(11) NOT NULL,
`perms_labels` smallint(6) NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Daten für Tabelle `groups`
--
INSERT INTO `groups` (`id`, `name`, `parent_id`, `comment`, `perms_system`, `perms_groups`, `perms_users`, `perms_self`, `perms_system_config`, `perms_system_database`, `perms_parts`, `perms_parts_name`, `perms_parts_description`, `perms_parts_instock`, `perms_parts_mininstock`, `perms_parts_footprint`, `perms_parts_storelocation`, `perms_parts_manufacturer`, `perms_parts_comment`, `perms_parts_order`, `perms_parts_orderdetails`, `perms_parts_prices`, `perms_parts_attachements`, `perms_devices`, `perms_devices_parts`, `perms_storelocations`, `perms_footprints`, `perms_categories`, `perms_suppliers`, `perms_manufacturers`, `perms_attachement_types`, `perms_tools`, `perms_labels`, `datetime_added`, `last_modified`) VALUES
(1, 'admins', NULL, 'Users of this group can do everything: Read, Write and Administrative actions.', 21, 1365, 87381, 85, 85, 21, 1431655765, 5, 5, 5, 5, 5, 5, 5, 5, 5, 325, 325, 325, 5461, 325, 5461, 5461, 5461, 5461, 5461, 1365, 1365, 85, '2023-01-07 18:31:48', '0000-00-00 00:00:00'),
(2, 'readonly', NULL, 'Users of this group can only read informations, use tools, and don\'t have access to administrative tools.', 42, 2730, 174762, 154, 170, 42, -1516939607, 9, 9, 9, 9, 9, 9, 9, 9, 9, 649, 649, 649, 1705, 649, 1705, 1705, 1705, 1705, 1705, 681, 1366, 165, '2023-01-07 18:31:48', '0000-00-00 00:00:00'),
(3, 'users', NULL, 'Users of this group, can edit part informations, create new ones, etc. but are not allowed to use administrative tools. (But can read current configuration, and see Server status)', 42, 2730, 109226, 89, 105, 41, 1431655765, 5, 5, 5, 5, 5, 5, 5, 5, 5, 325, 325, 325, 5461, 325, 5461, 5461, 5461, 5461, 5461, 1365, 1365, 85, '2023-01-07 18:31:48', '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `internal`
--
CREATE TABLE `internal` (
`keyName` char(30) CHARACTER SET ascii NOT NULL,
`keyValue` varchar(255) COLLATE utf8mb3_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `internal`
--
INSERT INTO `internal` (`keyName`, `keyValue`) VALUES
('dbVersion', '26');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `log`
--
CREATE TABLE `log` (
`id` int(11) NOT NULL,
`datetime` timestamp NOT NULL DEFAULT current_timestamp(),
`id_user` int(11) NOT NULL,
`level` tinyint(4) NOT NULL,
`type` smallint(6) NOT NULL,
`target_id` int(11) NOT NULL,
`target_type` smallint(6) NOT NULL,
`extra` mediumtext NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Daten für Tabelle `log`
--
INSERT INTO `log` (`id`, `datetime`, `id_user`, `level`, `type`, `target_id`, `target_type`, `extra`) VALUES
(1, '2023-01-07 18:31:48', 1, 4, 10, 0, 0, '{\"o\":0,\"n\":26,\"s\":true}'),
(2, '2023-01-07 18:32:13', 2, 6, 1, 2, 1, '{\"i\":\"::\"}'),
(3, '2023-01-07 18:32:29', 2, 6, 6, 1, 4, '[]'),
(4, '2023-01-07 18:32:53', 2, 6, 6, 1, 12, '[]'),
(5, '2023-01-07 18:33:26', 2, 6, 6, 1, 10, '{\"i\":0}');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `manufacturers`
--
CREATE TABLE `manufacturers` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`address` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`phone_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`fax_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`email_address` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`website` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`auto_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `orderdetails`
--
CREATE TABLE `orderdetails` (
`id` int(11) NOT NULL,
`part_id` int(11) NOT NULL,
`id_supplier` int(11) NOT NULL DEFAULT 0,
`supplierpartnr` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`obsolete` tinyint(1) DEFAULT 0,
`supplier_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `orderdetails`
--
INSERT INTO `orderdetails` (`id`, `part_id`, `id_supplier`, `supplierpartnr`, `obsolete`, `supplier_product_url`, `datetime_added`) VALUES
(1, 1, 1, 'BC547', 0, '', '2023-01-07 18:45:59'),
(2, 1, 1, 'Test', 0, '', '2023-01-07 18:46:09');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `parts`
--
CREATE TABLE `parts` (
`id` int(11) NOT NULL,
`id_category` int(11) NOT NULL DEFAULT 0,
`name` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`description` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`instock` int(11) NOT NULL DEFAULT 0,
`mininstock` int(11) NOT NULL DEFAULT 0,
`comment` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`visible` tinyint(1) NOT NULL,
`id_footprint` int(11) DEFAULT NULL,
`id_storelocation` int(11) DEFAULT NULL,
`order_orderdetails_id` int(11) DEFAULT NULL,
`order_quantity` int(11) NOT NULL DEFAULT 1,
`manual_order` tinyint(1) NOT NULL DEFAULT 0,
`id_manufacturer` int(11) DEFAULT NULL,
`id_master_picture_attachement` int(11) DEFAULT NULL,
`manufacturer_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`favorite` tinyint(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `parts`
--
INSERT INTO `parts` (`id`, `id_category`, `name`, `description`, `instock`, `mininstock`, `comment`, `visible`, `id_footprint`, `id_storelocation`, `order_orderdetails_id`, `order_quantity`, `manual_order`, `id_manufacturer`, `id_master_picture_attachement`, `manufacturer_product_url`, `datetime_added`, `last_modified`, `favorite`) VALUES
(1, 1, 'BC547', '', 0, 0, '', 0, NULL, NULL, NULL, 1, 0, NULL, NULL, '', '2023-01-07 18:33:26', '2023-01-07 18:33:26', 0);
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `pricedetails`
--
CREATE TABLE `pricedetails` (
`id` int(11) NOT NULL,
`orderdetails_id` int(11) NOT NULL,
`price` decimal(11,5) DEFAULT NULL,
`price_related_quantity` int(11) NOT NULL DEFAULT 1,
`min_discount_quantity` int(11) NOT NULL DEFAULT 1,
`manual_input` tinyint(1) NOT NULL DEFAULT 1,
`last_modified` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `pricedetails`
--
INSERT INTO `pricedetails` (`id`, `orderdetails_id`, `price`, `price_related_quantity`, `min_discount_quantity`, `manual_input`, `last_modified`) VALUES
(1, 2, '3.55000', 1, 1, 1, '2023-01-07 18:46:19');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `storelocations`
--
CREATE TABLE `storelocations` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`is_full` tinyint(1) NOT NULL DEFAULT 0,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `suppliers`
--
CREATE TABLE `suppliers` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`address` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`phone_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`fax_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`email_address` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`website` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`auto_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `suppliers`
--
INSERT INTO `suppliers` (`id`, `name`, `parent_id`, `address`, `phone_number`, `fax_number`, `email_address`, `website`, `auto_product_url`, `datetime_added`, `comment`, `last_modified`) VALUES
(1, 'Reichelt', NULL, '', '', '', '', '', '', '2023-01-07 18:32:53', '', '2023-01-07 18:32:53');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `users`
--
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`name` varchar(32) NOT NULL,
`password` varchar(255) DEFAULT NULL,
`first_name` tinytext DEFAULT NULL,
`last_name` tinytext DEFAULT NULL,
`department` tinytext DEFAULT NULL,
`email` tinytext DEFAULT NULL,
`need_pw_change` tinyint(1) NOT NULL DEFAULT 0,
`group_id` int(11) DEFAULT NULL,
`config_language` tinytext DEFAULT NULL,
`config_timezone` tinytext DEFAULT NULL,
`config_theme` tinytext DEFAULT NULL,
`config_currency` tinytext DEFAULT NULL,
`config_image_path` text NOT NULL,
`config_instock_comment_w` text NOT NULL,
`config_instock_comment_a` text NOT NULL,
`perms_system` int(11) NOT NULL,
`perms_groups` int(11) NOT NULL,
`perms_users` int(11) NOT NULL,
`perms_self` int(11) NOT NULL,
`perms_system_config` int(11) NOT NULL,
`perms_system_database` int(11) NOT NULL,
`perms_parts` bigint(11) NOT NULL,
`perms_parts_name` smallint(6) NOT NULL,
`perms_parts_description` smallint(6) NOT NULL,
`perms_parts_instock` smallint(6) NOT NULL,
`perms_parts_mininstock` smallint(6) NOT NULL,
`perms_parts_footprint` smallint(6) NOT NULL,
`perms_parts_storelocation` smallint(6) NOT NULL,
`perms_parts_manufacturer` smallint(6) NOT NULL,
`perms_parts_comment` smallint(6) NOT NULL,
`perms_parts_order` smallint(6) NOT NULL,
`perms_parts_orderdetails` smallint(6) NOT NULL,
`perms_parts_prices` smallint(6) NOT NULL,
`perms_parts_attachements` smallint(6) NOT NULL,
`perms_devices` int(11) NOT NULL,
`perms_devices_parts` int(11) NOT NULL,
`perms_storelocations` int(11) NOT NULL,
`perms_footprints` int(11) NOT NULL,
`perms_categories` int(11) NOT NULL,
`perms_suppliers` int(11) NOT NULL,
`perms_manufacturers` int(11) NOT NULL,
`perms_attachement_types` int(11) NOT NULL,
`perms_tools` int(11) NOT NULL,
`perms_labels` smallint(6) NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Daten für Tabelle `users`
--
INSERT INTO `users` (`id`, `name`, `password`, `first_name`, `last_name`, `department`, `email`, `need_pw_change`, `group_id`, `config_language`, `config_timezone`, `config_theme`, `config_currency`, `config_image_path`, `config_instock_comment_w`, `config_instock_comment_a`, `perms_system`, `perms_groups`, `perms_users`, `perms_self`, `perms_system_config`, `perms_system_database`, `perms_parts`, `perms_parts_name`, `perms_parts_description`, `perms_parts_instock`, `perms_parts_mininstock`, `perms_parts_footprint`, `perms_parts_storelocation`, `perms_parts_manufacturer`, `perms_parts_comment`, `perms_parts_order`, `perms_parts_orderdetails`, `perms_parts_prices`, `perms_parts_attachements`, `perms_devices`, `perms_devices_parts`, `perms_storelocations`, `perms_footprints`, `perms_categories`, `perms_suppliers`, `perms_manufacturers`, `perms_attachement_types`, `perms_tools`, `perms_labels`, `datetime_added`, `last_modified`) VALUES
(1, 'anonymous', '', '', '', '', '', 0, 2, NULL, NULL, NULL, NULL, '', '', '', 21844, 20480, 0, 0, 0, 0, 0, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21520, 21520, 21520, 20480, 21520, 20480, 20480, 20480, 20480, 20480, 21504, 20480, 0, '2023-01-07 18:31:48', '0000-00-00 00:00:00'),
(2, 'admin', '$2a$12$j0RKrKlx60bzX1DWMyXwjeaW.pe3bFjAK8ByIGnvjrRnET2JtsFoe$2a$12$j0RKrKlx60bzX1DWMyXwjeaW.pe3bFjAK8ByIGnvjrRnET2JtsFoe', '', '', '', '', 1, 1, NULL, NULL, NULL, NULL, '', '', '', 21845, 21845, 21845, 21, 85, 21, 349525, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 0, '2023-01-07 18:31:48', '0000-00-00 00:00:00');
--
-- Indizes der exportierten Tabellen
--
--
-- Indizes für die Tabelle `attachements`
--
ALTER TABLE `attachements`
ADD PRIMARY KEY (`id`),
ADD KEY `attachements_class_name_k` (`class_name`),
ADD KEY `attachements_element_id_k` (`element_id`),
ADD KEY `attachements_type_id_fk` (`type_id`);
--
-- Indizes für die Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
ADD PRIMARY KEY (`id`),
ADD KEY `attachement_types_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `categories`
--
ALTER TABLE `categories`
ADD PRIMARY KEY (`id`),
ADD KEY `categories_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `devices`
--
ALTER TABLE `devices`
ADD PRIMARY KEY (`id`),
ADD KEY `devices_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `device_parts`
--
ALTER TABLE `device_parts`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `device_parts_combination_uk` (`id_part`,`id_device`),
ADD KEY `device_parts_id_part_k` (`id_part`),
ADD KEY `device_parts_id_device_k` (`id_device`);
--
-- Indizes für die Tabelle `footprints`
--
ALTER TABLE `footprints`
ADD PRIMARY KEY (`id`),
ADD KEY `footprints_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `groups`
--
ALTER TABLE `groups`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- Indizes für die Tabelle `internal`
--
ALTER TABLE `internal`
ADD UNIQUE KEY `keyName` (`keyName`);
--
-- Indizes für die Tabelle `log`
--
ALTER TABLE `log`
ADD PRIMARY KEY (`id`),
ADD KEY `id_user` (`id_user`);
--
-- Indizes für die Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
ADD PRIMARY KEY (`id`),
ADD KEY `manufacturers_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `orderdetails`
--
ALTER TABLE `orderdetails`
ADD PRIMARY KEY (`id`),
ADD KEY `orderdetails_part_id_k` (`part_id`),
ADD KEY `orderdetails_id_supplier_k` (`id_supplier`);
--
-- Indizes für die Tabelle `parts`
--
ALTER TABLE `parts`
ADD PRIMARY KEY (`id`),
ADD KEY `parts_id_category_k` (`id_category`),
ADD KEY `parts_id_footprint_k` (`id_footprint`),
ADD KEY `parts_id_storelocation_k` (`id_storelocation`),
ADD KEY `parts_order_orderdetails_id_k` (`order_orderdetails_id`),
ADD KEY `parts_id_manufacturer_k` (`id_manufacturer`),
ADD KEY `favorite` (`favorite`);
--
-- Indizes für die Tabelle `pricedetails`
--
ALTER TABLE `pricedetails`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `pricedetails_combination_uk` (`orderdetails_id`,`min_discount_quantity`),
ADD KEY `pricedetails_orderdetails_id_k` (`orderdetails_id`);
--
-- Indizes für die Tabelle `storelocations`
--
ALTER TABLE `storelocations`
ADD PRIMARY KEY (`id`),
ADD KEY `storelocations_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `suppliers`
--
ALTER TABLE `suppliers`
ADD PRIMARY KEY (`id`),
ADD KEY `suppliers_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- AUTO_INCREMENT für exportierte Tabellen
--
--
-- AUTO_INCREMENT für Tabelle `attachements`
--
ALTER TABLE `attachements`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- AUTO_INCREMENT für Tabelle `categories`
--
ALTER TABLE `categories`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `devices`
--
ALTER TABLE `devices`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `device_parts`
--
ALTER TABLE `device_parts`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `footprints`
--
ALTER TABLE `footprints`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `groups`
--
ALTER TABLE `groups`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- AUTO_INCREMENT für Tabelle `log`
--
ALTER TABLE `log`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- AUTO_INCREMENT für Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `orderdetails`
--
ALTER TABLE `orderdetails`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- AUTO_INCREMENT für Tabelle `parts`
--
ALTER TABLE `parts`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `pricedetails`
--
ALTER TABLE `pricedetails`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `storelocations`
--
ALTER TABLE `storelocations`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `suppliers`
--
ALTER TABLE `suppliers`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `users`
--
ALTER TABLE `users`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- Constraints der exportierten Tabellen
--
--
-- Constraints der Tabelle `attachements`
--
ALTER TABLE `attachements`
ADD CONSTRAINT `attachements_type_id_fk` FOREIGN KEY (`type_id`) REFERENCES `attachement_types` (`id`);
--
-- Constraints der Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
ADD CONSTRAINT `attachement_types_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `attachement_types` (`id`);
--
-- Constraints der Tabelle `categories`
--
ALTER TABLE `categories`
ADD CONSTRAINT `categories_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `categories` (`id`);
--
-- Constraints der Tabelle `devices`
--
ALTER TABLE `devices`
ADD CONSTRAINT `devices_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `devices` (`id`);
--
-- Constraints der Tabelle `footprints`
--
ALTER TABLE `footprints`
ADD CONSTRAINT `footprints_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `footprints` (`id`);
--
-- Constraints der Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
ADD CONSTRAINT `manufacturers_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `manufacturers` (`id`);
--
-- Constraints der Tabelle `parts`
--
ALTER TABLE `parts`
ADD CONSTRAINT `parts_id_footprint_fk` FOREIGN KEY (`id_footprint`) REFERENCES `footprints` (`id`),
ADD CONSTRAINT `parts_id_manufacturer_fk` FOREIGN KEY (`id_manufacturer`) REFERENCES `manufacturers` (`id`),
ADD CONSTRAINT `parts_id_storelocation_fk` FOREIGN KEY (`id_storelocation`) REFERENCES `storelocations` (`id`),
ADD CONSTRAINT `parts_order_orderdetails_id_fk` FOREIGN KEY (`order_orderdetails_id`) REFERENCES `orderdetails` (`id`);
--
-- Constraints der Tabelle `storelocations`
--
ALTER TABLE `storelocations`
ADD CONSTRAINT `storelocations_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `storelocations` (`id`);
--
-- Constraints der Tabelle `suppliers`
--
ALTER TABLE `suppliers`
ADD CONSTRAINT `suppliers_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `suppliers` (`id`);
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View file

@ -1,54 +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/>.
#
#!/bin/bash
# This script is used to test the legacy import of Part-DB
SQL_FILES_TO_TEST=("db_minimal.sql" "db_jbtronics.sql")
DB_NAME="legacy_db_test"
DB_USER="root"
DB_PASSWORD="root"
# Iterate over all given SQL files and import them into the mysql database with the given name, drop the database if it already exists before
for SQL_FILE in "${SQL_FILES_TO_TEST[@]}"
do
echo "Testing for $SQL_FILE"
mysql -u $DB_USER --password=$DB_PASSWORD -e "DROP DATABASE IF EXISTS $DB_NAME; CREATE DATABASE $DB_NAME;"
# If the last command failed, exit the script
if [ $? -ne 0 ]; then
echo "Failed to create database $DB_NAME"
exit 1
fi
# Import the SQL file into the database. The file pathes are relative to the current script location
mysql -u $DB_USER --password=$DB_PASSWORD $DB_NAME < .github/assets/legacy_import/$SQL_FILE
# If the last command failed, exit the script
if [ $? -ne 0 ]; then
echo "Failed to import $SQL_FILE into database $DB_NAME"
exit 1
fi
# Run doctrine migrations, this will migrate the database to the current version. This process should not fail
php bin/console doctrine:migrations:migrate -n
# If the last command failed, exit the script
if [ $? -ne 0 ]; then
echo "Failed to migrate database $DB_NAME"
exit 1
fi
done

View file

@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View file

@ -1,92 +0,0 @@
name: Build assets artifact
permissions:
contents: read
on:
push:
branches:
- '*'
- "!l10n_*" # Dont test localization branches
pull_request:
branches:
- '*'
- "!l10n_*"
jobs:
assets_artifact_build:
name: Build assets artifact
runs-on: ubuntu-22.04
env:
APP_ENV: prod
steps:
- uses: actions/checkout@v6
- 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
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-dev -a
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: '20'
- name: Install yarn dependencies
run: yarn install
- name: Build frontend
run: yarn build
- name: Remove node_modules/ folder
run: rm -rf node_modules/
- name: Zip the current folder
run: zip -r /tmp/partdb_with_assets.zip . -x .git/\* -x var/\* -x node_modules/\*
- name: Zip only the assets folder (include public/build/ and vendor/)
run: zip -r /tmp/partdb_assets.zip public/build/ vendor/
- name: Upload assets artifact
uses: actions/upload-artifact@v5
with:
name: Only dependencies and built assets
path: /tmp/partdb_assets.zip
- name: Upload full artifact
uses: actions/upload-artifact@v5
with:
name: Full Part-DB including dependencies and built assets
path: /tmp/partdb_with_assets.zip

54
.github/workflows/codeql-analysis.yml vendored Normal file
View 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@v2
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@v1
# 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@v1
# 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@v1

View file

@ -1,18 +1,18 @@
name: Docker Image Build name: Docker Image Build
permissions:
contents: read
on: on:
#schedule: #schedule:
# - cron: '0 10 * * *' # everyday at 10am # - cron: '0 10 * * *' # everyday at 10am
push: push:
branches: branches:
- '**' - '**'
- '!l10n_**' - '!I10n_**'
tags: tags:
- 'v*.*.*' - 'v*.*.*'
- 'v*.*.*-**' - 'v*.*.*-**'
pull_request:
branches:
- 'master'
jobs: jobs:
docker: docker:
@ -20,17 +20,18 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v6 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: |
jbtronics/part-db1 jbtronics/part-db1
# Mark the image build from master as latest (as we dont have really releases yet) # Mark the image build from master as latest (as we dont have really releases yet)
tags: | tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=edge,branch=master type=edge,branch=master
type=ref,event=branch, type=ref,event=branch,
type=ref,event=tag, type=ref,event=tag,
@ -52,23 +53,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

View file

@ -1,80 +0,0 @@
name: Docker Image Build (FrankenPHP)
permissions:
contents: read
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@v6
-
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

View file

@ -1,17 +1,14 @@
name: Static analysis name: Static analysis
permissions:
contents: read
on: on:
push: push:
branches: branches:
- '*' - '*'
- "!l10n_*" # Dont test localization branches - "!i10n_*" # Dont test localization branches
pull_request: pull_request:
branches: branches:
- '*' - '*'
- "!l10n_*" - "!i10n_*"
jobs: jobs:
phpstan: phpstan:
@ -19,22 +16,14 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v6 - 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') }}
@ -50,19 +39,17 @@ 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@v3
- 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

View file

@ -1,28 +1,25 @@
name: PHPUnit Tests name: PHPUnit Tests
permissions:
contents: read
on: on:
push: push:
branches: branches:
- '*' - '*'
- "!l10n_*" # Dont test localization branches - "!i10n_*" # Dont test localization branches
pull_request: pull_request:
branches: branches:
- '*' - '*'
- "!l10n_*" - "!i10n_*"
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.2', '8.3', '8.4', '8.5' ] 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
@ -30,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@v6 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
@ -81,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@v1
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') }}
@ -92,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 }}
@ -104,9 +86,9 @@ 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@v6 uses: actions/setup-node@v3
with: with:
node-version: '20' node-version: '16'
- name: Install yarn dependencies - name: Install yarn dependencies
run: yarn install run: yarn install
@ -116,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
@ -147,11 +131,3 @@ 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
run: bash .github/assets/legacy_import/test_legacy_import.sh
if: matrix.db-type == 'mysql' && matrix.php-versions == '8.2'
env:
DATABASE_URL: mysql://root:root@localhost:3306/legacy_db

10
.gitignore vendored
View file

@ -2,7 +2,6 @@
/.env.local /.env.local
/.env.local.php /.env.local.php
/.env.*.local /.env.*.local
/.env.local.bak
/config/secrets/prod/prod.decrypt.private.php /config/secrets/prod/prod.decrypt.private.php
/public/bundles/ /public/bundles/
/var/ /var/
@ -42,12 +41,5 @@ yarn-error.log
###> phpunit/phpunit ### ###> phpunit/phpunit ###
/phpunit.xml /phpunit.xml
/.phpunit.cache/ .phpunit.result.cache
###< phpunit/phpunit ### ###< phpunit/phpunit ###
###> phpstan/phpstan ###
phpstan.neon
###< phpstan/phpstan ###
.claude/
CLAUDE.md

View file

@ -4,23 +4,25 @@ Thank you for consider to contribute to Part-DB!
Please read the text below, so your contributed content can be contributed easily to Part-DB. Please read the text below, so your contributed content can be contributed easily to Part-DB.
You can contribute to Part-DB in various ways: You can contribute to Part-DB in various ways:
* Report bugs and request new features via [issues](https://github.com/Part-DB/Part-DB-server/issues) * Report bugs and request new features via [issues](https://github.com/Part-DB/Part-DB-symfony/issues)
* Improve translations (via https://part-db.crowdin.com/part-db) * Improve translations (via https://part-db.crowdin.com/part-db)
* Improve code (either PHP, Javascript or HTML templates) by creating a [pull request](https://github.com/Part-DB/Part-DB-server/pulls) * Improve code (either PHP, Javascript or HTML templates) by creating a [pull request](https://github.com/Part-DB/Part-DB-symfony/pulls)
## Translations ## Translations
The recommended way to create/improve translations is to use the online platform [Crowdin](https://part-db.crowdin.com/part-db). The recommended way to create/improve translations is to use the online platform [Crowdin](https://part-db.crowdin.com/part-db).
Register an account there and join the Part-DB team. Register an account there and join the Part-DB team.
If you want to start translation for a new language that does not have an entry on Crowdin yet, send a message to `@jbtronics`. If you want to start translation for a new language that does not have an entry on Crowdin yet, send an message to `@jbtronics`.
Part-DB uses translation keys (e.g. part.info.title) that are sorted by their usage, so you will most likely have to lookup, how the key Part-DB uses translation keys (e.g. part.info.title) that are sorted by their usage, so you will most likely have to lookup, how the key
was translated in other languages (this is possible via the "Other languages" dropdown in the translation editor). was translated in other languages (this is possible via the "Other languages" dropdown in the translation editor).
Translation keys can be extracted from templates and PHP codes by running `bin/console translation:extract`.
## Project structure ## Project structure
Part-DB uses symfony's recommended [project structure](https://symfony.com/doc/current/best_practices.html). Part-DB uses symfony's recommended [project structure](https://symfony.com/doc/current/best_practices.html).
Interesting folders are: Interesting folders are:
* `public`: Everything in this directory will be publicly accessible via web. Use this folder to serve static images. * `public`: Everything in this directory will be publicy accessible via web. Use this folder to serve static images.
* `assets`: The frontend assets are saved here. You can find the javascript and CSS code here. * `assets`: The frontend assets are saved here. You can find the javascript and CSS code here.
* `src`: Part-DB's PHP code is saved here. Note that the sub directories are structured by the classes purposes (so use `Controller` Controllers, `Entities` for Database models, etc.) * `src`: Part-DB's PHP code is saved here. Note that the sub directories are structured by the classes purposes (so use `Controller` Controllers, `Entities` for Database models, etc.)
* `translations`: The translations used in Part-DB are saved here * `translations`: The translations used in Part-DB are saved here
@ -43,13 +45,13 @@ Part-DB uses [Easy Coding Standard](https://github.com/symplify/easy-coding-stan
* To check your code for valid code style run `vendor/bin/ecs check src/` * To check your code for valid code style run `vendor/bin/ecs check src/`
* To fix violations run `vendor/bin/ecs check src/` (please checks afterwards if the code is valid afterwards) * To fix violations run `vendor/bin/ecs check src/` (please checks afterwards if the code is valid afterwards)
## GitHub actions ## TravisCI
Part-DB uses GitHub actions to run various tests and checks on the code: Part-DB has a [Travis-CI](https://travis-ci.com/Part-DB/Part-DB-symfony) instance running, that checks for every commit and contribute if the following things are working:
* Yarn dependencies can compile * Yarn dependencies can compile
* PHPunit tests run successful * PHPunit tests run successful
* Config files, translations and templates has valid syntax * Config files, translations and templates has valid syntax
* Doctrine schema valid * Doctrine schema valid
* No known vulnerable dependencies are used * No known vulnerable dependecies are used
* Static analysis successful (phpstan with `--level=2`) * Static analysis successful (phpstan with `--level=2`)
Further the code coverage of the PHPunit tests is determined and uploaded to [CodeCov](https://codecov.io/gh/Part-DB/Part-DB-server). Further the code coverage of the PHPunit tests is determined.

View file

@ -1,64 +1,22 @@
ARG BASE_IMAGE=debian:bookworm-slim FROM debian:bullseye-slim
ARG PHP_VERSION=8.4
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 libapache2-mod-php8.1 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_22.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,6 +27,8 @@ 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"; \
@ -76,87 +36,66 @@ RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \ ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.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 mpm_prefork
RUN a2dismod mpm_event && a2enmod mpm_prefork
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/apache2/conf.d/symfony-recommended.ini
# Increase upload limit and enable preloading (disabled for now, as it does not seem to work properly, and require prod env anyway) # Increase upload limit
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 } > /etc/php/8.1/apache2/conf.d/partdb.ini
log_limit=8096
EOF
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 +103,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"]

View file

@ -1,121 +0,0 @@
FROM dunglas/frankenphp:1-php8.4 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/*;
RUN set -eux; \
# Prepare keyrings directory
mkdir -p /etc/apt/keyrings; \
\
# Import Yarn GPG key
curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg \
| tee /etc/apt/keyrings/yarn.gpg >/dev/null; \
chmod 644 /etc/apt/keyrings/yarn.gpg; \
\
# Add Yarn repo with signed-by
echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian stable main" \
| tee /etc/apt/sources.list.d/yarn.list; \
\
# Run NodeSource setup script (unchanged)
curl -sL https://deb.nodesource.com/setup_22.x | bash -; \
\
# Install Node.js + Yarn
apt-get update; \
apt-get install -y --no-install-recommends \
nodejs \
yarn; \
\
# Cleanup
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

View file

@ -1,91 +0,0 @@
# PartDB Makefile for Test Environment Management
.PHONY: help deps-install lint format format-check test coverage pre-commit all test-typecheck \
test-setup test-clean test-db-create test-db-migrate test-cache-clear test-fixtures test-run test-reset \
section-dev dev-setup dev-clean dev-db-create dev-db-migrate dev-cache-clear dev-warmup dev-reset
# Default target
help: ## Show this help
@awk 'BEGIN {FS = ":.*##"}; /^[a-zA-Z0-9][a-zA-Z0-9_-]+:.*##/ {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
# Dependencies
deps-install: ## Install PHP dependencies with unlimited memory
@echo "📦 Installing PHP dependencies..."
COMPOSER_MEMORY_LIMIT=-1 composer install
yarn install
@echo "✅ Dependencies installed"
# Complete test environment setup
test-setup: test-clean test-db-create test-db-migrate test-fixtures ## Complete test setup (clean, create DB, migrate, fixtures)
@echo "✅ Test environment setup complete!"
# Clean test environment
test-clean: ## Clean test cache and database files
@echo "🧹 Cleaning test environment..."
rm -rf var/cache/test
rm -f var/app_test.db
@echo "✅ Test environment cleaned"
# Create test database
test-db-create: ## Create test database (if not exists)
@echo "🗄️ Creating test database..."
-php bin/console doctrine:database:create --if-not-exists --env test || echo "⚠️ Database creation failed (expected for SQLite) - continuing..."
# Run database migrations for test environment
test-db-migrate: ## Run database migrations for test environment
@echo "🔄 Running database migrations..."
COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env test
# Clear test cache
test-cache-clear: ## Clear test cache
@echo "🗑️ Clearing test cache..."
rm -rf var/cache/test
@echo "✅ Test cache cleared"
# Load test fixtures
test-fixtures: ## Load test fixtures
@echo "📦 Loading test fixtures..."
php bin/console partdb:fixtures:load -n --env test
# Run PHPUnit tests
test-run: ## Run PHPUnit tests
@echo "🧪 Running tests..."
php bin/phpunit
# Quick test reset (clean + migrate + fixtures, skip DB creation)
test-reset: test-cache-clear test-db-migrate test-fixtures
@echo "✅ Test environment reset complete!"
test-typecheck: ## Run static analysis (PHPStan)
@echo "🧪 Running type checks..."
COMPOSER_MEMORY_LIMIT=-1 composer phpstan
# Development helpers
dev-setup: dev-clean dev-db-create dev-db-migrate dev-warmup ## Complete development setup (clean, create DB, migrate, warmup)
@echo "✅ Development environment setup complete!"
dev-clean: ## Clean development cache and database files
@echo "🧹 Cleaning development environment..."
rm -rf var/cache/dev
rm -f var/app_dev.db
@echo "✅ Development environment cleaned"
dev-db-create: ## Create development database (if not exists)
@echo "🗄️ Creating development database..."
-php bin/console doctrine:database:create --if-not-exists --env dev || echo "⚠️ Database creation failed (expected for SQLite) - continuing..."
dev-db-migrate: ## Run database migrations for development environment
@echo "🔄 Running database migrations..."
COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env dev
dev-cache-clear: ## Clear development cache
@echo "🗑️ Clearing development cache..."
rm -rf var/cache/dev
@echo "✅ Development cache cleared"
dev-warmup: ## Warm up development cache
@echo "🔥 Warming up development cache..."
COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=1G bin/console cache:warmup --env dev -n
dev-reset: dev-cache-clear dev-db-migrate ## Quick development reset (cache clear + migrate)
@echo "✅ Development environment reset complete!"

170
README.md
View file

@ -1,166 +1,134 @@
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Part-DB/Part-DB-symfony/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Part-DB/Part-DB-symfony/?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Part-DB/Part-DB-symfony/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Part-DB/Part-DB-symfony/?branch=master)
![PHPUnit Tests](https://github.com/Part-DB/Part-DB-symfony/workflows/PHPUnit%20Tests/badge.svg) ![PHPUnit Tests](https://github.com/Part-DB/Part-DB-symfony/workflows/PHPUnit%20Tests/badge.svg)
![Static analysis](https://github.com/Part-DB/Part-DB-symfony/workflows/Static%20analysis/badge.svg) ![Static analysis](https://github.com/Part-DB/Part-DB-symfony/workflows/Static%20analysis/badge.svg)
[![codecov](https://codecov.io/gh/Part-DB/Part-DB-server/branch/master/graph/badge.svg)](https://codecov.io/gh/Part-DB/Part-DB-server) [![codecov](https://codecov.io/gh/Part-DB/Part-DB-symfony/branch/master/graph/badge.svg)](https://codecov.io/gh/Part-DB/Part-DB-symfony)
![GitHub License](https://img.shields.io/github/license/Part-DB/Part-DB-symfony) ![GitHub License](https://img.shields.io/github/license/Part-DB/Part-DB-symfony)
![PHP Version](https://img.shields.io/badge/PHP-%3E%3D%208.2-green) ![PHP Version](https://img.shields.io/badge/PHP-%3E%3D%207.4-green)
![Docker Pulls](https://img.shields.io/docker/pulls/jbtronics/part-db1) ![Docker Pulls](https://img.shields.io/docker/pulls/jbtronics/part-db1)
![Docker Build Status](https://github.com/Part-DB/Part-DB-symfony/workflows/Docker%20Image%20Build/badge.svg) ![Docker Build Status](https://github.com/Part-DB/Part-DB-symfony/workflows/Docker%20Image%20Build/badge.svg)
[![Crowdin](https://badges.crowdin.net/e/8325196085d4bee8c04b75f7c915452a/localized.svg)](https://part-db.crowdin.com/part-db) [![Crowdin](https://badges.crowdin.net/e/8325196085d4bee8c04b75f7c915452a/localized.svg)](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)** *When updgrading from a version from before 2022-11-27, please read [this](https://github.com/Part-DB/Part-DB-symfony/discussions/193) before upgrading!*
# 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. Currently it is still missing some (less) features from the old version (see [UPGRADE.md](./UPGRADE.md)) for more details, but also many huge improvements and advantages compared to the old version. If you start completly new with Part-DB it is recommended that you use the version from this repository, as it is actively developed.
(Version < 1.0) based on a modern framework and is the recommended version to use.
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-symfony/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 Every change to the master branch gets automatically deployed, so it represents the currenct development progress and is
may not be completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading maybe not completly 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/parts_list.png">
## Features ## Features
* Inventory managment 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 informations. 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. * Import/Export system (partial working)
* Optional support for single sign-on (SSO) via SAML (using an intermediate service * Project managment: 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
like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server) * Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older versions.
* Import/Export system for parts and data structure. BOM import for projects from KiCAD is supported. * Responsive design: You can use Part-DB on your PC, your tablet and your smartphone using the same interface.
* Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build * MySQL and SQLite (experimental) supported as database backends
this project and directly withdraw all components needed from DB
* Event log: Track what changes happen to your inventory, track which user does what. Revert your parts to older
versions.
* 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
* 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 this 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.2** * 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** (>= 20.0) is needed.
## Installation ## Installation
**Caution:** It is possible to upgrade the old Part-DB databases.
Anyhow, the migrations that will be made, are not compatible with the old Part-DB versions, so you must not use the old Part-DB versions with the new database, or the DB could become corrupted.
Also after the migration it is not possible to go back to the old database scheme, so make sure to make a backup of your database beforehand.
See [UPGRADE](UPGRADE.md) for more infos.
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 setup Part-DB via docker is described [here](https://github.com/Part-DB/Part-DB-symfony/blob/master/docs/docker/docker-install.md).
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 general hints for installtion, see [here](docs/installation/installation_guide-debian.md) for a detailed guide how to install Part-DB on Debian/Ubuntu.**
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 informations.
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,
however, you can set the initial 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.
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 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.
If you are using a reverse proxy, you have to ensure that the proxies set the `X-Forwarded-*` headers correctly, or you ## Useful console commands
will get HTTP/HTTPS mixup and wrong hostnames. Part-DB provides some command consoles which can be invoked by `php bin/console [command]`. You can get help for every command with the parameter `--help`.
If the reverse proxy is on a different server (or it cannot access Part-DB via localhost) you have to set Useful commands are:
the `TRUSTED_PROXIES` env variable to match your reverse proxy's IP address (or IP block). You can do this in * `php bin/console partdb:users:set-password [username]`: Sets a new password for the user with the given username. Useful if you forget the password to your Part-DB instance.
your `.env.local` or (when using docker) in your `docker-compose.yml` file. * `php bin/console partdb:logs:show`: Show last activty log on console. Use `-x` options, to include extra column.
* `php bin/console partdb:currencies:update-exchange-rates`: Update the exchange rates of your currencies from internet. Setup this to be run in a cronjob to always get up-to-date exchange rates.
If you dont use Euro as base currency, you have to setup an fixer.io API key in `.env.local`.
* `php bin/console partdb:attachments:clean-unused`: Removes all unused files (files without an associated attachment) in attachments folder.
Normally Part-DB should be able to delete the attachment file, if you delete the attachment, but if you have some obsolete files left over from legacy Part-DB you can remove them safely with this command.
* `php bin/console partdb:check-requirements`: Checks if the required dependencies are installed and gives you recommendations for optimization
* `php bin/console cache:clear`: Remove and rebuild all caches. If you encounter some weird issues in Part-DB, it maybe helps to run this command.
* `php bin/console doctrine:migrations:up-to-date`: Check if your database is up to date.
* Normally a random password is generated when the admin user is created during inital database creation.
You can set the inital admin password, by setting the `INITIAL_ADMIN_PW` env var.
## 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-symfony/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.
See [LICENSE](https://github.com/Part-DB/Part-DB-server/blob/master/LICENSE) for more information. See [LICENSE](https://github.com/Part-DB/Part-DB-symfony/blob/master/LICENSE) for more informations.

View file

@ -2,11 +2,8 @@
## Supported Versions ## Supported Versions
Only the most recent release of Part-DB is supported. This is an alpha version without releases yet. Only the newest version from the master branch is supported.
(Unreleased) development versions are not supported and might contain security vulnerabilities, which might not be
fixed before the next release. However, if you find a security vulnerability in a development version, please report it
## Reporting a Vulnerability ## Reporting a Vulnerability
If you find a security vulnerability, report a vulnerability in the [security section of GitHub](https://github.com/Part-DB/Part-DB-server/security/advisories) or contact the maintainer directly (Email: security@part-db.de) If you find an vulnerability contact the maintainer directly (Email: security@part-db.de).

35
UPGRADE.md Normal file
View file

@ -0,0 +1,35 @@
# Upgrade from legacy Versions (Part-DB 0.5/0.6)
This document describes how to upgrade from an old Part-DB version (Part-DB 0.6 or older) to Part-DB 1.0.
The instructions on how to install the new version or upgrade from Part-DB 1.0 to a newer version, see
[README](README.md).
## Breaking Changes
Please note that there are some breaking changes with the new version.
It is tried to keep the breaking changes as small as possible, so they should not have much impact for the most users:
* PHP 7.2.5 is required now (Part-DB 0.5 required PHP 5.4+, Part-DB 0.6 PHP 7.0).
PHP 7.2.5 (or newer) is shipped by all current major Linux distros now (and can be installed by third party sources on others),
Releases are available for Windows too, so almost everybody should be able to use PHP 7.2.5
* Console access highly required. The installation of composer and frontend dependencies require console access, also
the managment commands are using CLI, so you should have console access on your server.
* Markdown/HTML is now used instead of BBCode for rich text in description and command fields.
It is possible to migrate your existing BBCode to Markdown via `php bin/console php bin/console partdb:migrations:convert-bbcode`.
* Server exceptions are not logged to Event log anymore. For security reasons (exceptions can contain sensitive informations)
exceptions are only logged to server log (by default under './var/log'), so only the server admins can access it.
* Profile labels are now saved in Database (before they were saved in a seperate JSON file). The profiles of legacy Part-DB versions can not be imported into new Part-DB 1.0
* Label placeholders now use the `[[PLACEHOLDER]]` format instead of `%PLACEHOLDER%`. Also some placeholders has changed.
## Upgrade process
1. Upgrade your existing Part-DB version the newest Part-DB 0.5.* version (in the moment Part-DB 0.5.8), like
described in the old Part-DB's repository.
2. Make a backup of your database. If somethings goes wrong during migration, you can use this backup to start over.
3. Setup the new Part-DB like described on [README](README.md) in section Installation. In `.env.local` enter the URL
to your old Part-DB database.
4. Run `php bin/console partdb:migrations:convert-bbcode` to convert the BBCode used in comments and part description to the newly used markdown.
5. Copy the content of `data/media` from the old Part-DB version into `public/media` in the new version.
6. Run 'php bin/console cache:clear'
You should now be able to access Part-DB and log in using your old credentials.
**It is not possible to access the database using the old Part-DB version.
If you do so, this could damage your database.** Therefore it is recommended to remove the old Part-DB version.

View file

@ -1 +1 @@
2.2.1 1.0.0-beta.1

View file

@ -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);

File diff suppressed because one or more lines are too long

View file

@ -1,63 +1,66 @@
import {ClassicEditor} from 'ckeditor5' /**
import {Alignment} from 'ckeditor5'; * @license Copyright (c) 2014-2022, CKSource Holding sp. z o.o. All rights reserved.
import {Autoformat} from 'ckeditor5'; * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
import {Base64UploadAdapter} from 'ckeditor5'; */
import {BlockQuote} from 'ckeditor5'; import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js';
import {Bold} from 'ckeditor5'; import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment.js';
import {Code} from 'ckeditor5'; import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat.js';
import {CodeBlock} from 'ckeditor5'; import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/base64uploadadapter.js';
import {Essentials} from 'ckeditor5'; import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote.js';
import {FindAndReplace} from 'ckeditor5'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
import {FontBackgroundColor} from 'ckeditor5'; import Code from '@ckeditor/ckeditor5-basic-styles/src/code.js';
import {FontColor} from 'ckeditor5'; import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock.js';
import {FontFamily} from 'ckeditor5'; import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
import {FontSize} from 'ckeditor5'; import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace.js';
import {GeneralHtmlSupport} from 'ckeditor5'; import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor.js';
import {Heading} from 'ckeditor5'; import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor.js';
import {Highlight} from 'ckeditor5'; import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily.js';
import {HorizontalLine} from 'ckeditor5'; import FontSize from '@ckeditor/ckeditor5-font/src/fontsize.js';
import {HtmlComment} from 'ckeditor5'; import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport.js';
import {HtmlEmbed} from 'ckeditor5'; import Heading from '@ckeditor/ckeditor5-heading/src/heading.js';
import {Image} from 'ckeditor5'; import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight.js';
import {ImageResize} from 'ckeditor5'; import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline.js';
import {ImageStyle} from 'ckeditor5'; import HtmlComment from '@ckeditor/ckeditor5-html-support/src/htmlcomment.js';
import {ImageToolbar} from 'ckeditor5'; import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed.js';
import {ImageUpload} from 'ckeditor5'; import Image from '@ckeditor/ckeditor5-image/src/image.js';
import {Indent} from 'ckeditor5'; import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize.js';
import {IndentBlock} from 'ckeditor5'; import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle.js';
import {Italic} from 'ckeditor5'; import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar.js';
import {Link} from 'ckeditor5'; import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload.js';
import {LinkImage} from 'ckeditor5'; import Indent from '@ckeditor/ckeditor5-indent/src/indent.js';
import {List} from 'ckeditor5'; import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock.js';
import {ListProperties} from 'ckeditor5'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
import {Markdown} from 'ckeditor5'; import Link from '@ckeditor/ckeditor5-link/src/link.js';
import {MediaEmbed} from 'ckeditor5'; import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage.js';
import {MediaEmbedToolbar} from 'ckeditor5'; import List from '@ckeditor/ckeditor5-list/src/list.js';
import {Paragraph} from 'ckeditor5'; import ListProperties from '@ckeditor/ckeditor5-list/src/listproperties.js';
import {PasteFromOffice} from 'ckeditor5'; import Markdown from '@ckeditor/ckeditor5-markdown-gfm/src/markdown.js';
import {RemoveFormat} from 'ckeditor5'; import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed.js';
import {SourceEditing} from 'ckeditor5'; import MediaEmbedToolbar from '@ckeditor/ckeditor5-media-embed/src/mediaembedtoolbar.js';
import {SpecialCharacters} from 'ckeditor5'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
import {SpecialCharactersArrows} from 'ckeditor5'; import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice.js';
import {SpecialCharactersCurrency} from 'ckeditor5'; import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js';
import {SpecialCharactersEssentials} from 'ckeditor5'; import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js';
import {SpecialCharactersLatin} from 'ckeditor5'; import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters.js';
import {SpecialCharactersMathematical} from 'ckeditor5'; import SpecialCharactersArrows from '@ckeditor/ckeditor5-special-characters/src/specialcharactersarrows.js';
import {SpecialCharactersText} from 'ckeditor5'; import SpecialCharactersCurrency from '@ckeditor/ckeditor5-special-characters/src/specialcharacterscurrency.js';
import {Strikethrough} from 'ckeditor5'; import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials.js';
import {Subscript} from 'ckeditor5'; import SpecialCharactersLatin from '@ckeditor/ckeditor5-special-characters/src/specialcharacterslatin.js';
import {Superscript} from 'ckeditor5'; import SpecialCharactersMathematical from '@ckeditor/ckeditor5-special-characters/src/specialcharactersmathematical.js';
import {Table} from 'ckeditor5'; import SpecialCharactersText from '@ckeditor/ckeditor5-special-characters/src/specialcharacterstext.js';
import {TableCaption} from 'ckeditor5'; import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough.js';
import {TableCellProperties} from 'ckeditor5'; import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript.js';
import {TableColumnResize} from 'ckeditor5'; import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript.js';
import {TableProperties} from 'ckeditor5'; import Table from '@ckeditor/ckeditor5-table/src/table.js';
import {TableToolbar} from 'ckeditor5'; import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption.js';
import {Underline} from 'ckeditor5'; import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
import {WordCount} from 'ckeditor5'; import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize.js';
import {EditorWatchdog} from 'ckeditor5'; import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar.js';
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js';
import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount.js';
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog.js';
import PartDBLabel from "./plugins/PartDBLabel/PartDBLabel"; import PartDBLabel from "./plugins/PartDBLabel/PartDBLabel";
import SpecialCharactersGreek from "./plugins/special_characters_emoji";
class Editor extends ClassicEditor {} class Editor extends ClassicEditor {}
@ -119,8 +122,7 @@ Editor.builtinPlugins = [
Underline, Underline,
WordCount, WordCount,
PartDBLabel, PartDBLabel
SpecialCharactersGreek
]; ];
// Editor configuration. // Editor configuration.
@ -179,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
}, },

View file

@ -2,69 +2,68 @@
* @license Copyright (c) 2014-2022, CKSource Holding sp. z o.o. All rights reserved. * @license Copyright (c) 2014-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/ */
import {ClassicEditor} from 'ckeditor5'; import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js';
import {Alignment} from 'ckeditor5'; import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment.js';
import {Autoformat} from 'ckeditor5'; import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat.js';
import {Base64UploadAdapter} from 'ckeditor5'; import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/base64uploadadapter.js';
import {BlockQuote} from 'ckeditor5'; import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote.js';
import {Bold} from 'ckeditor5'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
import {Code} from 'ckeditor5'; import Code from '@ckeditor/ckeditor5-basic-styles/src/code.js';
import {CodeBlock} from 'ckeditor5'; import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock.js';
import {Essentials} from 'ckeditor5'; import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
import {FindAndReplace} from 'ckeditor5'; import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace.js';
import {FontBackgroundColor} from 'ckeditor5'; import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor.js';
import {FontColor} from 'ckeditor5'; import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor.js';
import {FontFamily} from 'ckeditor5'; import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily.js';
import {FontSize} from 'ckeditor5'; import FontSize from '@ckeditor/ckeditor5-font/src/fontsize.js';
import {GeneralHtmlSupport} from 'ckeditor5'; import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport.js';
import {Heading} from 'ckeditor5'; import Heading from '@ckeditor/ckeditor5-heading/src/heading.js';
import {Highlight} from 'ckeditor5'; import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight.js';
import {HorizontalLine} from 'ckeditor5'; import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline.js';
import {HtmlComment} from 'ckeditor5'; import HtmlComment from '@ckeditor/ckeditor5-html-support/src/htmlcomment.js';
import {HtmlEmbed} from 'ckeditor5'; import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed.js';
import {Image} from 'ckeditor5'; import Image from '@ckeditor/ckeditor5-image/src/image.js';
import {ImageResize} from 'ckeditor5'; import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize.js';
import {ImageStyle} from 'ckeditor5'; import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle.js';
import {ImageToolbar} from 'ckeditor5'; import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar.js';
import {ImageUpload} from 'ckeditor5'; import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload.js';
import {Indent} from 'ckeditor5'; import Indent from '@ckeditor/ckeditor5-indent/src/indent.js';
import {IndentBlock} from 'ckeditor5'; import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock.js';
import {Italic} from 'ckeditor5'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
import {Link} from 'ckeditor5'; import Link from '@ckeditor/ckeditor5-link/src/link.js';
import {LinkImage} from 'ckeditor5'; import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage.js';
import {List} from 'ckeditor5'; import List from '@ckeditor/ckeditor5-list/src/list.js';
import {ListProperties} from 'ckeditor5'; import ListProperties from '@ckeditor/ckeditor5-list/src/listproperties.js';
import {Markdown} from 'ckeditor5'; import Markdown from '@ckeditor/ckeditor5-markdown-gfm/src/markdown.js';
import {MediaEmbed} from 'ckeditor5'; import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed.js';
import {MediaEmbedToolbar} from 'ckeditor5'; import MediaEmbedToolbar from '@ckeditor/ckeditor5-media-embed/src/mediaembedtoolbar.js';
import {Paragraph} from 'ckeditor5'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
import {PasteFromOffice} from 'ckeditor5'; import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice.js';
import {RemoveFormat} from 'ckeditor5'; import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js';
import {SourceEditing} from 'ckeditor5'; import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js';
import {SpecialCharacters} from 'ckeditor5'; import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters.js';
import {SpecialCharactersArrows} from 'ckeditor5'; import SpecialCharactersArrows from '@ckeditor/ckeditor5-special-characters/src/specialcharactersarrows.js';
import {SpecialCharactersCurrency} from 'ckeditor5'; import SpecialCharactersCurrency from '@ckeditor/ckeditor5-special-characters/src/specialcharacterscurrency.js';
import {SpecialCharactersEssentials} from 'ckeditor5'; import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials.js';
import {SpecialCharactersLatin} from 'ckeditor5'; import SpecialCharactersLatin from '@ckeditor/ckeditor5-special-characters/src/specialcharacterslatin.js';
import {SpecialCharactersMathematical} from 'ckeditor5'; import SpecialCharactersMathematical from '@ckeditor/ckeditor5-special-characters/src/specialcharactersmathematical.js';
import {SpecialCharactersText} from 'ckeditor5'; import SpecialCharactersText from '@ckeditor/ckeditor5-special-characters/src/specialcharacterstext.js';
import {Strikethrough} from 'ckeditor5'; import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough.js';
import {Subscript} from 'ckeditor5'; import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript.js';
import {Superscript} from 'ckeditor5'; import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript.js';
import {Table} from 'ckeditor5'; import Table from '@ckeditor/ckeditor5-table/src/table.js';
import {TableCaption} from 'ckeditor5'; import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption.js';
import {TableCellProperties} from 'ckeditor5'; import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
import {TableColumnResize} from 'ckeditor5'; import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize.js';
import {TableProperties} from 'ckeditor5'; import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
import {TableToolbar} from 'ckeditor5'; import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar.js';
import {Underline} from 'ckeditor5'; import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js';
import {WordCount} from 'ckeditor5'; import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount.js';
import {EditorWatchdog} from 'ckeditor5'; import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog.js';
import {TodoList} from 'ckeditor5'; import TodoList from '@ckeditor/ckeditor5-list/src/todolist';
import ExtendedMarkdown from "./plugins/extendedMarkdown.js"; import ExtendedMarkdown from "./plugins/extendedMarkdown.js";
import SpecialCharactersGreek from "./plugins/special_characters_emoji"; import SpecialCharactersEmoji from "./plugins/special_characters_emoji";
import {Mention, Emoji} from "ckeditor5";
class Editor extends ClassicEditor {} class Editor extends ClassicEditor {}
@ -118,11 +117,9 @@ Editor.builtinPlugins = [
Underline, Underline,
TodoList, TodoList,
Mention, Emoji,
//Our own extensions //Our own extensions
ExtendedMarkdown, ExtendedMarkdown,
SpecialCharactersGreek SpecialCharactersEmoji
]; ];
// Editor configuration. // Editor configuration.
@ -151,7 +148,6 @@ Editor.defaultConfig = {
'indent', 'indent',
'|', '|',
'specialCharacters', 'specialCharacters',
"emoji",
'horizontalLine', 'horizontalLine',
'|', '|',
'imageUpload', 'imageUpload',

View file

@ -2,36 +2,35 @@
* @license Copyright (c) 2014-2022, CKSource Holding sp. z o.o. All rights reserved. * @license Copyright (c) 2014-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/ */
import {ClassicEditor} from 'ckeditor5'; import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js';
import {Autoformat} from 'ckeditor5'; import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat.js';
import {AutoLink} from 'ckeditor5'; import AutoLink from '@ckeditor/ckeditor5-link/src/autolink.js';
import {Bold} from 'ckeditor5'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
import {Code} from 'ckeditor5'; import Code from '@ckeditor/ckeditor5-basic-styles/src/code.js';
import {Essentials} from 'ckeditor5'; import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
import {FindAndReplace} from 'ckeditor5'; import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace.js';
import {Highlight} from 'ckeditor5'; import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight.js';
import {Italic} from 'ckeditor5'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
import {Link} from 'ckeditor5'; import Link from '@ckeditor/ckeditor5-link/src/link.js';
import {Paragraph} from 'ckeditor5'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
import {RemoveFormat} from 'ckeditor5'; import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js';
import {SourceEditing} from 'ckeditor5'; import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js';
import {SpecialCharacters} from 'ckeditor5'; import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters.js';
import {SpecialCharactersArrows} from 'ckeditor5'; import SpecialCharactersArrows from '@ckeditor/ckeditor5-special-characters/src/specialcharactersarrows.js';
import {SpecialCharactersCurrency} from 'ckeditor5'; import SpecialCharactersCurrency from '@ckeditor/ckeditor5-special-characters/src/specialcharacterscurrency.js';
import {SpecialCharactersEssentials} from 'ckeditor5'; import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials.js';
import {SpecialCharactersLatin} from 'ckeditor5'; import SpecialCharactersLatin from '@ckeditor/ckeditor5-special-characters/src/specialcharacterslatin.js';
import {SpecialCharactersMathematical} from 'ckeditor5'; import SpecialCharactersMathematical from '@ckeditor/ckeditor5-special-characters/src/specialcharactersmathematical.js';
import {SpecialCharactersText} from 'ckeditor5'; import SpecialCharactersText from '@ckeditor/ckeditor5-special-characters/src/specialcharacterstext.js';
import {Strikethrough} from 'ckeditor5'; import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough.js';
import {Subscript} from 'ckeditor5'; import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript.js';
import {Superscript} from 'ckeditor5'; import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript.js';
import {Underline} from 'ckeditor5'; import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js';
import {EditorWatchdog} from 'ckeditor5'; import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog.js';
import {Mention, Emoji} from "ckeditor5";
import ExtendedMarkdownInline from "./plugins/extendedMarkdownInline"; import ExtendedMarkdownInline from "./plugins/extendedMarkdownInline";
import SingleLinePlugin from "./plugins/singleLine"; import SingleLinePlugin from "./plugins/singleLine";
import SpecialCharactersGreek from "./plugins/special_characters_emoji"; import SpecialCharactersEmoji from "./plugins/special_characters_emoji";
class Editor extends ClassicEditor {} class Editor extends ClassicEditor {}
@ -63,8 +62,7 @@ Editor.builtinPlugins = [
ExtendedMarkdownInline, ExtendedMarkdownInline,
SingleLinePlugin, SingleLinePlugin,
SpecialCharactersGreek, SpecialCharactersEmoji
Mention, Emoji
]; ];
// Editor configuration. // Editor configuration.
@ -83,7 +81,6 @@ Editor.defaultConfig = {
'link', 'link',
'code', 'code',
'specialCharacters', 'specialCharacters',
'emoji',
'|', '|',
'undo', 'undo',
'redo', 'redo',

View file

@ -22,7 +22,7 @@ import PartDBLabelEditing from "./PartDBLabelEditing";
import "./PartDBLabel.css"; import "./PartDBLabel.css";
import {Plugin} from "ckeditor5"; import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
export default class PartDBLabel extends Plugin { export default class PartDBLabel extends Plugin {
static get requires() { static get requires() {

View file

@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Command} from 'ckeditor5'; import Command from '@ckeditor/ckeditor5-core/src/command';
export default class PartDBLabelCommand extends Command { export default class PartDBLabelCommand extends Command {
execute( { value } ) { execute( { value } ) {

View file

@ -17,11 +17,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Plugin} from 'ckeditor5'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import PartDBLabelCommand from "./PartDBLabelCommand"; import PartDBLabelCommand from "./PartDBLabelCommand";
import { toWidget } from 'ckeditor5'; import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
import {Widget} from 'ckeditor5'; import Widget from '@ckeditor/ckeditor5-widget/src/widget';
export default class PartDBLabelEditing extends Plugin { export default class PartDBLabelEditing extends Plugin {
static get requires() { // ADDED static get requires() { // ADDED

View file

@ -17,15 +17,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Plugin} from 'ckeditor5'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
require('./lang/de.js'); require('./lang/de.js');
require('./lang/en.js');
import { addListToDropdown, createDropdown } from 'ckeditor5'; import { addListToDropdown, createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
import {Collection} from 'ckeditor5'; import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import {UIModel} from 'ckeditor5'; import Model from '@ckeditor/ckeditor5-ui/src/model';
export default class PartDBLabelUI extends Plugin { export default class PartDBLabelUI extends Plugin {
init() { init() {
@ -77,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'],
@ -86,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'],
] ]
}, },
{ {
@ -101,8 +96,6 @@ const PLACEHOLDERS = [
['[[AMOUNT]]', 'Lot amount'], ['[[AMOUNT]]', 'Lot amount'],
['[[LOCATION]]', 'Storage location'], ['[[LOCATION]]', 'Storage location'],
['[[LOCATION_FULL]]', 'Storage location (Full path)'], ['[[LOCATION_FULL]]', 'Storage location (Full path)'],
['[[OWNER]]', 'Full name of the lot owner'],
['[[OWNER_USERNAME]]', 'Username of the lot owner'],
] ]
}, },
{ {
@ -117,8 +110,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'],
['[[OWNER]]', 'Full name of the location owner'],
['[[OWNER_USERNAME]]', 'Username of the location owner'],
] ]
}, },
{ {
@ -129,8 +120,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'],
] ]
}, },
{ {
@ -152,28 +141,18 @@ const PLACEHOLDERS = [
function getDropdownItemsDefinitions(t) { function getDropdownItemsDefinitions(t) {
const itemDefinitions = new Collection(); const itemDefinitions = new Collection();
let first = true;
for ( const group of PLACEHOLDERS) { for ( const group of PLACEHOLDERS) {
//Add group header //Add group header
//Skip separator for first group
if (!first) {
itemDefinitions.add({ itemDefinitions.add({
'type': 'separator', 'type': 'separator',
model: new UIModel( { model: new Model( {
withText: true, withText: true,
}) })
}); });
} else {
first = false;
}
itemDefinitions.add({ itemDefinitions.add({
type: 'button', type: 'button',
model: new UIModel( { model: new Model( {
label: t(group.label), label: t(group.label),
withText: true, withText: true,
isEnabled: false, isEnabled: false,
@ -184,7 +163,7 @@ function getDropdownItemsDefinitions(t) {
for ( const entry of group.entries) { for ( const entry of group.entries) {
const definition = { const definition = {
type: 'button', type: 'button',
model: new UIModel( { model: new Model( {
commandParam: entry[0], commandParam: entry[0],
label: t(entry[1]), label: t(entry[1]),
tooltip: entry[0], tooltip: entry[0],

View file

@ -17,9 +17,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {add} from "ckeditor5"; // Make sure that the global object is defined. If not, define it.
window.CKEDITOR_TRANSLATIONS = window.CKEDITOR_TRANSLATIONS || {};
add( "de", { // Make sure that the dictionary for Polish translations exist.
window.CKEDITOR_TRANSLATIONS[ 'de' ] = window.CKEDITOR_TRANSLATIONS[ 'de' ] || {};
window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary = window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary || {};
// Extend the dictionary for Polish translations with your translations:
Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
'Label Placeholder': 'Label Platzhalter', 'Label Placeholder': 'Label Platzhalter',
'Part': 'Bauteil', 'Part': 'Bauteil',
@ -33,7 +39,6 @@ add( "de", {
'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',
@ -42,9 +47,6 @@ add( "de", {
'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',
@ -53,8 +55,6 @@ add( "de", {
'Lot amount': 'Lot Menge', 'Lot amount': 'Lot Menge',
'Storage location': 'Lagerort', 'Storage location': 'Lagerort',
'Storage location (Full path)': 'Lagerort (Vollständiger Pfad)', 'Storage location (Full path)': 'Lagerort (Vollständiger Pfad)',
'Full name of the lot owner': 'Name des Besitzers des Lots',
'Username of the lot owner': 'Benutzername des Besitzers des Lots',
'Barcodes': 'Barcodes', 'Barcodes': 'Barcodes',
@ -63,16 +63,12 @@ add( "de", {
'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',
'Full path': 'Vollständiger Pfad', 'Full path': 'Vollständiger Pfad',
'Parent name': 'Name des Übergeordneten Elements', 'Parent name': 'Name des Übergeordneten Elements',
'Parent full path': 'Ganzer Pfad des Übergeordneten Elements', 'Parent full path': 'Ganzer Pfad des Übergeordneten Elements',
'Full name of the location owner': 'Name des Besitzers des Lagerorts',
'Username of the location owner': 'Benutzername des Besitzers des Lagerorts',
'Username': 'Benutzername', 'Username': 'Benutzername',
'Username (including name)': 'Benutzername (inklusive Name)', 'Username (including name)': 'Benutzername (inklusive Name)',
@ -82,4 +78,5 @@ add( "de", {
'Instance name': 'Instanzname', 'Instance name': 'Instanzname',
'Target type': 'Zieltyp', 'Target type': 'Zieltyp',
'URL of this Part-DB instance': 'URL dieser Part-DB Instanz', 'URL of this Part-DB instance': 'URL dieser Part-DB Instanz',
} ); } );

View file

@ -1,84 +0,0 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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 {add} from "ckeditor5";
add( "en", {
'Label Placeholder': 'Label placeholder',
'Part': 'Part',
'Database ID': 'Database ID',
'Part name': 'Part name',
'Category': 'Category',
'Category (Full path)': 'Category (full path)',
'Manufacturer': 'Manufacturer',
'Manufacturer (Full path)': 'Manufacturer (full path)',
'Footprint': 'Footprint',
'Footprint (Full path)': 'Footprint (full path)',
'Mass': 'Mass',
'Manufacturer Product Number (MPN)': 'Manufacturer Product Number (MPN)',
'Internal Part Number (IPN)': 'Internal Part Number (IPN)',
'Tags': 'Tags',
'Manufacturing status': 'Manufacturing status',
'Description': 'Description',
'Description (plain text)': 'Description (plain text)',
'Comment': 'Comment',
'Comment (plain text)': 'Comment (plain text)',
'Last modified datetime': 'Last modified datetime',
'Creation datetime': 'Creation datetime',
'IPN as QR code': 'IPN as QR code',
'IPN as Code 128 barcode': 'IPN as Code 128 barcode',
'IPN as Code 39 barcode': 'IPN as Code 39 barcode',
'Lot ID': 'Lot ID',
'Lot name': 'Lot name',
'Lot comment': 'Lot comment',
'Lot expiration date': 'Lot expiration date',
'Lot amount': 'Lot amount',
'Storage location': 'Storage location',
'Storage location (Full path)': 'Storage location (full path)',
'Full name of the lot owner': 'Full name of the lot owner',
'Username of the lot owner': 'Username of the lot owner',
'Barcodes': 'Barcodes',
'Content of the 1D barcodes (like Code 39)': 'Content of the 1D barcodes (like Code 39)',
'Content of the 2D barcodes (QR codes)': 'Content of the 2D barcodes (QR codes)',
'QR code linking to this element': 'QR code linking to this element',
'Code 128 barcode linking to this element': 'Code 128 barcode linking to this element',
'Code 39 barcode linking to this element': 'Code 39 barcode linking to this element',
'Code 93 barcode linking to this element': 'Code 93 barcode linking to this element',
'Datamatrix code linking to this element': 'Datamatrix code linking to this element',
'Location ID': 'Location ID',
'Name': 'Name',
'Full path': 'Full path',
'Parent name': 'Parent name',
'Parent full path': 'Parent full path',
'Full name of the location owner': 'Full name of the location owner',
'Username of the location owner': 'Username of the location owner',
'Username': 'Username',
'Username (including name)': 'Username (including name)',
'Current datetime': 'Current datetime',
'Current date': 'Current date',
'Current time': 'Current time',
'Instance name': 'Instance name',
'Target type': 'Target type',
'URL of this Part-DB instance': 'URL of this Part-DB instance',
} );

View file

@ -17,7 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Plugin, MarkdownGfmDataProcessor } from 'ckeditor5'; import { Plugin } from 'ckeditor5/src/core';
import GFMDataProcessor from '@ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor';
const ALLOWED_TAGS = [ const ALLOWED_TAGS = [
//Common elements //Common elements
@ -33,6 +34,7 @@ const ALLOWED_TAGS = [
//Block elements //Block elements
'span', 'span',
'p',
'img', 'img',
@ -55,7 +57,7 @@ export default class ExtendedMarkdown extends Plugin {
constructor( editor ) { constructor( editor ) {
super( editor ); super( editor );
editor.data.processor = new MarkdownGfmDataProcessor( editor.data.viewDocument ); editor.data.processor = new GFMDataProcessor( editor.data.viewDocument );
for (const tag of ALLOWED_TAGS) { for (const tag of ALLOWED_TAGS) {
editor.data.processor.keepHtml(tag); editor.data.processor.keepHtml(tag);
} }

View file

@ -17,8 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Plugin} from 'ckeditor5'; import { Plugin } from 'ckeditor5/src/core';
import {MarkdownGfmDataProcessor} from 'ckeditor5'; import GFMDataProcessor from '@ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor';
const ALLOWED_TAGS = [ const ALLOWED_TAGS = [
//Common elements //Common elements
@ -46,7 +46,7 @@ export default class ExtendedMarkdownInline extends Plugin {
constructor( editor ) { constructor( editor ) {
super( editor ); super( editor );
editor.data.processor = new MarkdownGfmDataProcessor( editor.data.viewDocument ); editor.data.processor = new GFMDataProcessor( editor.data.viewDocument );
for (const tag of ALLOWED_TAGS) { for (const tag of ALLOWED_TAGS) {
editor.data.processor.keepHtml(tag); editor.data.processor.keepHtml(tag);
} }

View file

@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Plugin} from 'ckeditor5'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
export default class SingleLinePlugin extends Plugin { export default class SingleLinePlugin extends Plugin {
init() { init() {

View file

@ -17,78 +17,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import SpecialCharacters from 'ckeditor5'; import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters';
import SpecialCharactersEssentials from 'ckeditor5'; import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials';
import {Plugin} from 'ckeditor5'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
export default class SpecialCharactersGreek extends Plugin { const emoji = require('emoji.json');
export default class SpecialCharactersEmoji extends Plugin {
init() { init() {
const editor = this.editor; const editor = this.editor;
const specialCharsPlugin = editor.plugins.get('SpecialCharacters'); const specialCharsPlugin = editor.plugins.get('SpecialCharacters');
//Add greek characters to special characters specialCharsPlugin.addItems('Emoji', this.getEmojis());
specialCharsPlugin.addItems('Greek', this.getGreek());
} }
getGreek() { getEmojis() {
return [ //Map our emoji data to the format the plugin expects
{ title: 'Alpha', character: 'Α' }, return emoji.map(emoji => {
{ title: 'Beta', character: 'Β' }, return {
{ title: 'Gamma', character: 'Γ' }, title: emoji.name,
{ title: 'Delta', character: 'Δ' }, character: emoji.char
{ title: 'Epsilon', character: 'Ε' }, };
{ title: 'Zeta', character: 'Ζ' }, });
{ title: 'Eta', character: 'Η' },
{ title: 'Theta', character: 'Θ' },
{ title: 'Iota', character: 'Ι' },
{ title: 'Kappa', character: 'Κ' },
{ title: 'Lambda', character: 'Λ' },
{ title: 'Mu', character: 'Μ' },
{ title: 'Nu', character: 'Ν' },
{ title: 'Xi', character: 'Ξ' },
{ title: 'Omicron', character: 'Ο' },
{ title: 'Pi', character: 'Π' },
{ title: 'Rho', character: 'Ρ' },
{ title: 'Sigma', character: 'Σ' },
{ title: 'Tau', character: 'Τ' },
{ title: 'Upsilon', character: 'Υ' },
{ title: 'Phi', character: 'Φ' },
{ title: 'Chi', character: 'Χ' },
{ title: 'Psi', character: 'Ψ' },
{ title: 'Omega', character: 'Ω' },
{ title: 'alpha', character: 'α' },
{ title: 'beta', character: 'β' },
{ title: 'gamma', character: 'γ' },
{ title: 'delta', character: 'δ' },
{ title: 'epsilon', character: 'ε' },
{ title: 'zeta', character: 'ζ' },
{ title: 'eta', character: 'η' },
{ title: 'theta', character: 'θ' },
{ title: 'alternate theta', character: 'ϑ' },
{ title: 'iota', character: 'ι' },
{ title: 'kappa', character: 'κ' },
{ title: 'lambda', character: 'λ' },
{ title: 'mu', character: 'μ' },
{ title: 'nu', character: 'ν' },
{ title: 'xi', character: 'ξ' },
{ title: 'omicron', character: 'ο' },
{ title: 'pi', character: 'π' },
{ title: 'rho', character: 'ρ' },
{ title: 'sigma', character: 'σ' },
{ title: 'tau', character: 'τ' },
{ title: 'upsilon', character: 'υ' },
{ title: 'phi', character: 'φ' },
{ title: 'chi', character: 'χ' },
{ title: 'psi', character: 'ψ' },
{ title: 'omega', character: 'ω' },
{ title: 'digamma', character: 'Ϝ' },
{ title: 'stigma', character: 'Ϛ' },
{ title: 'heta', character: 'Ͱ' },
{ title: 'sampi', character: 'Ϡ' },
{ title: 'koppa', character: 'Ϟ' },
{ title: 'san', character: 'Ϻ' },
];
} }
} }

View file

@ -1,359 +0,0 @@
import { Controller } from "@hotwired/stimulus"
import { generateCsrfHeaders } from "./csrf_protection_controller"
export default class extends Controller {
static targets = ["progressBar", "progressText"]
static values = {
jobId: Number,
partId: Number,
researchUrl: String,
researchAllUrl: String,
markCompletedUrl: String,
markSkippedUrl: String,
markPendingUrl: String
}
connect() {
// Auto-refresh progress if job is in progress
if (this.hasProgressBarTarget) {
this.startProgressUpdates()
}
// Restore scroll position after page reload (if any)
this.restoreScrollPosition()
}
getHeaders() {
const headers = {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
// Add CSRF headers if available
const form = document.querySelector('form')
if (form) {
const csrfHeaders = generateCsrfHeaders(form)
Object.assign(headers, csrfHeaders)
}
return headers
}
async fetchWithErrorHandling(url, options = {}, timeout = 30000) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
...options,
headers: { ...this.getHeaders(), ...options.headers },
signal: controller.signal
})
clearTimeout(timeoutId)
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Server error (${response.status}): ${errorText}`)
}
return await response.json()
} catch (error) {
clearTimeout(timeoutId)
if (error.name === 'AbortError') {
throw new Error('Request timed out. Please try again.')
} else if (error.message.includes('Failed to fetch')) {
throw new Error('Network error. Please check your connection and try again.')
} else {
throw error
}
}
}
disconnect() {
if (this.progressInterval) {
clearInterval(this.progressInterval)
}
}
startProgressUpdates() {
// Progress updates are handled via page reload for better reliability
// No need for periodic updates since state changes trigger page refresh
}
restoreScrollPosition() {
const savedPosition = sessionStorage.getItem('bulkImportScrollPosition')
if (savedPosition) {
// Restore scroll position after a small delay to ensure page is fully loaded
setTimeout(() => {
window.scrollTo(0, parseInt(savedPosition))
// Clear the saved position so it doesn't interfere with normal navigation
sessionStorage.removeItem('bulkImportScrollPosition')
}, 100)
}
}
async markCompleted(event) {
const partId = event.currentTarget.dataset.partId
try {
const url = this.markCompletedUrlValue.replace('__PART_ID__', partId)
const data = await this.fetchWithErrorHandling(url, { method: 'POST' })
if (data.success) {
this.updateProgressDisplay(data)
this.markRowAsCompleted(partId)
if (data.job_completed) {
this.showJobCompletedMessage()
}
} else {
this.showErrorMessage(data.error || 'Failed to mark part as completed')
}
} catch (error) {
console.error('Error marking part as completed:', error)
this.showErrorMessage(error.message || 'Failed to mark part as completed')
}
}
async markSkipped(event) {
const partId = event.currentTarget.dataset.partId
const reason = prompt('Reason for skipping (optional):') || ''
try {
const url = this.markSkippedUrlValue.replace('__PART_ID__', partId)
const data = await this.fetchWithErrorHandling(url, {
method: 'POST',
body: JSON.stringify({ reason })
})
if (data.success) {
this.updateProgressDisplay(data)
this.markRowAsSkipped(partId)
} else {
this.showErrorMessage(data.error || 'Failed to mark part as skipped')
}
} catch (error) {
console.error('Error marking part as skipped:', error)
this.showErrorMessage(error.message || 'Failed to mark part as skipped')
}
}
async markPending(event) {
const partId = event.currentTarget.dataset.partId
try {
const url = this.markPendingUrlValue.replace('__PART_ID__', partId)
const data = await this.fetchWithErrorHandling(url, { method: 'POST' })
if (data.success) {
this.updateProgressDisplay(data)
this.markRowAsPending(partId)
} else {
this.showErrorMessage(data.error || 'Failed to mark part as pending')
}
} catch (error) {
console.error('Error marking part as pending:', error)
this.showErrorMessage(error.message || 'Failed to mark part as pending')
}
}
updateProgressDisplay(data) {
if (this.hasProgressBarTarget) {
this.progressBarTarget.style.width = `${data.progress}%`
this.progressBarTarget.setAttribute('aria-valuenow', data.progress)
}
if (this.hasProgressTextTarget) {
this.progressTextTarget.textContent = `${data.completed_count} / ${data.total_count} completed`
}
}
markRowAsCompleted(partId) {
// Save scroll position and refresh page to show updated state
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
window.location.reload()
}
markRowAsSkipped(partId) {
// Save scroll position and refresh page to show updated state
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
window.location.reload()
}
markRowAsPending(partId) {
// Save scroll position and refresh page to show updated state
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
window.location.reload()
}
showJobCompletedMessage() {
const alert = document.createElement('div')
alert.className = 'alert alert-success alert-dismissible fade show'
alert.innerHTML = `
<i class="fas fa-check-circle"></i>
Job completed! All parts have been processed.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`
const container = document.querySelector('.card-body')
container.insertBefore(alert, container.firstChild)
}
async researchPart(event) {
event.preventDefault()
event.stopPropagation()
const partId = event.currentTarget.dataset.partId
const spinner = event.currentTarget.querySelector(`[data-research-spinner="${partId}"]`)
const button = event.currentTarget
// Show loading state
if (spinner) {
spinner.style.display = 'inline-block'
}
button.disabled = true
try {
const url = this.researchUrlValue.replace('__PART_ID__', partId)
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 30000) // 30 second timeout
const response = await fetch(url, {
method: 'POST',
headers: this.getHeaders(),
signal: controller.signal
})
clearTimeout(timeoutId)
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Server error (${response.status}): ${errorText}`)
}
const data = await response.json()
if (data.success) {
this.showSuccessMessage(`Research completed for part. Found ${data.results_count} results.`)
// Save scroll position and reload to show updated results
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
window.location.reload()
} else {
this.showErrorMessage(data.error || 'Research failed')
}
} catch (error) {
console.error('Error researching part:', error)
if (error.name === 'AbortError') {
this.showErrorMessage('Research timed out. Please try again.')
} else if (error.message.includes('Failed to fetch')) {
this.showErrorMessage('Network error. Please check your connection and try again.')
} else {
this.showErrorMessage(error.message || 'Research failed due to an unexpected error')
}
} finally {
// Hide loading state
if (spinner) {
spinner.style.display = 'none'
}
button.disabled = false
}
}
async researchAllParts(event) {
event.preventDefault()
event.stopPropagation()
const spinner = document.getElementById('research-all-spinner')
const button = event.currentTarget
// Show loading state
if (spinner) {
spinner.style.display = 'inline-block'
}
button.disabled = true
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 120000) // 2 minute timeout for bulk operations
const response = await fetch(this.researchAllUrlValue, {
method: 'POST',
headers: this.getHeaders(),
signal: controller.signal
})
clearTimeout(timeoutId)
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Server error (${response.status}): ${errorText}`)
}
const data = await response.json()
if (data.success) {
this.showSuccessMessage(`Research completed for ${data.researched_count} parts.`)
// Save scroll position and reload to show updated results
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
window.location.reload()
} else {
this.showErrorMessage(data.error || 'Bulk research failed')
}
} catch (error) {
console.error('Error researching all parts:', error)
if (error.name === 'AbortError') {
this.showErrorMessage('Bulk research timed out. This may happen with large batches. Please try again or process smaller batches.')
} else if (error.message.includes('Failed to fetch')) {
this.showErrorMessage('Network error. Please check your connection and try again.')
} else {
this.showErrorMessage(error.message || 'Bulk research failed due to an unexpected error')
}
} finally {
// Hide loading state
if (spinner) {
spinner.style.display = 'none'
}
button.disabled = false
}
}
showSuccessMessage(message) {
this.showToast('success', message)
}
showErrorMessage(message) {
this.showToast('error', message)
}
showToast(type, message) {
// Create a simple alert that doesn't disrupt layout
const alertId = 'alert-' + Date.now()
const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-triangle'
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger'
const alertHTML = `
<div class="alert ${alertClass} alert-dismissible fade show position-fixed"
style="top: 20px; right: 20px; z-index: 9999; max-width: 400px;"
id="${alertId}">
<i class="fas ${iconClass} me-2"></i>
${message}
<button type="button" class="btn-close" onclick="this.parentElement.remove()" aria-label="Close"></button>
</div>
`
// Add alert to body
document.body.insertAdjacentHTML('beforeend', alertHTML)
// Auto-remove after 5 seconds
setTimeout(() => {
const alertElement = document.getElementById(alertId)
if (alertElement) {
alertElement.remove()
}
}, 5000)
}
}

View file

@ -1,92 +0,0 @@
import { Controller } from "@hotwired/stimulus"
import { generateCsrfHeaders } from "./csrf_protection_controller"
export default class extends Controller {
static values = {
deleteUrl: String,
stopUrl: String,
deleteConfirmMessage: String,
stopConfirmMessage: String
}
connect() {
// Controller initialized
}
getHeaders() {
const headers = {
'X-Requested-With': 'XMLHttpRequest'
}
// Add CSRF headers if available
const form = document.querySelector('form')
if (form) {
const csrfHeaders = generateCsrfHeaders(form)
Object.assign(headers, csrfHeaders)
}
return headers
}
async deleteJob(event) {
const jobId = event.currentTarget.dataset.jobId
const confirmMessage = this.deleteConfirmMessageValue || 'Are you sure you want to delete this job?'
if (confirm(confirmMessage)) {
try {
const deleteUrl = this.deleteUrlValue.replace('__JOB_ID__', jobId)
const response = await fetch(deleteUrl, {
method: 'DELETE',
headers: this.getHeaders()
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`HTTP ${response.status}: ${errorText}`)
}
const data = await response.json()
if (data.success) {
location.reload()
} else {
alert('Error deleting job: ' + (data.error || 'Unknown error'))
}
} catch (error) {
console.error('Error deleting job:', error)
alert('Error deleting job: ' + error.message)
}
}
}
async stopJob(event) {
const jobId = event.currentTarget.dataset.jobId
const confirmMessage = this.stopConfirmMessageValue || 'Are you sure you want to stop this job?'
if (confirm(confirmMessage)) {
try {
const stopUrl = this.stopUrlValue.replace('__JOB_ID__', jobId)
const response = await fetch(stopUrl, {
method: 'POST',
headers: this.getHeaders()
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`HTTP ${response.status}: ${errorText}`)
}
const data = await response.json()
if (data.success) {
location.reload()
} else {
alert('Error stopping job: ' + (data.error || 'Unknown error'))
}
} catch (error) {
console.error('Error stopping job:', error)
alert('Error stopping job: ' + error.message)
}
}
}
}

View file

@ -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();
} }
} }

View file

@ -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();
} }
} }

View file

@ -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,12 +43,9 @@ 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')) {
// test if link is absolute
var r = new RegExp('^(?:[a-z+]+:)?//', 'i');
if (r.test(a.getAttribute('href'))) {
//Mark all links as external //Mark all links as external
a.classList.add('link-external'); a.classList.add('link-external');
//Open links in new tag //Open links in new tag
@ -66,7 +53,6 @@ export default class MarkdownController extends Controller {
//Dont track //Dont track
a.setAttribute('rel', 'noopener'); a.setAttribute('rel', 'noopener');
} }
}
//Apply bootstrap styles to tables //Apply bootstrap styles to tables
for(let table of this.element.querySelectorAll('table')) { for(let table of this.element.querySelectorAll('table')) {
@ -93,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,
}); });
}*/ }
} }

View file

@ -1,81 +0,0 @@
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
// Use `form.requestSubmit()` to ensure that the submit event is triggered. Using `form.submit()` will not trigger the event
// and thus this event-listener will not be executed.
document.addEventListener('submit', function (event) {
generateCsrfToken(event.target);
}, true);
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
document.addEventListener('turbo:submit-start', function (event) {
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
Object.keys(h).map(function (k) {
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
});
});
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
document.addEventListener('turbo:submit-end', function (event) {
removeCsrfToken(event.detail.formSubmission.formElement);
});
export function generateCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
let csrfToken = csrfField.value;
if (!csrfCookie && nameCheck.test(csrfToken)) {
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
}
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
if (csrfCookie && tokenCheck.test(csrfToken)) {
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
export function generateCsrfHeaders (formElement) {
const headers = {};
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return headers;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
headers[csrfCookie] = csrfField.value;
}
return headers;
}
export function removeCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
/* stimulusFetch: 'lazy' */
export default 'csrf-protection-controller';

View file

@ -23,31 +23,18 @@ 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 dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
let settings = { let settings = {
persistent: false, persistent: false,
create: true, create: true,
maxItems: 1, maxItems: 1,
createOnBlur: true, createOnBlur: true,
selectOnTab: true,
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin //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', delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
dropdownParent: dropdownParent,
render: { render: {
item: (data, escape) => { item: (data, escape) => {
return '<span>' + escape(data.label) + '</span>'; return '<span>' + escape(data.label) + '</span>';
@ -58,12 +45,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": {}
} }
}; };
@ -97,10 +78,4 @@ export default class extends Controller {
this._tomSelect = new TomSelect(this.element, settings); this._tomSelect = new TomSelect(this.element, settings);
} }
disconnect() {
super.disconnect();
//Destroy the TomSelect instance
this._tomSelect.destroy();
}
} }

View file

@ -23,32 +23,10 @@ import { default as FullEditor } from "../../ckeditor/markdown_full";
import { default as SingleLineEditor} from "../../ckeditor/markdown_single_line"; import { default as SingleLineEditor} from "../../ckeditor/markdown_single_line";
import { default as HTMLLabelEditor } from "../../ckeditor/html_label"; import { default as HTMLLabelEditor } from "../../ckeditor/html_label";
import {EditorWatchdog} from 'ckeditor5'; import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog';
import "ckeditor5/ckeditor5.css";;
import "../../css/components/ckeditor.css"; import "../../css/components/ckeditor.css";
const translationContext = require.context(
'ckeditor5/translations',
false,
//Only load the translation files we will really need
/(de|it|fr|ru|ja|cs|da|zh|pl|hu)\.js$/
);
function loadTranslation(language) {
if (!language || language === 'en') {
return null;
}
const lang = language.slice(0, 2);
const path = `./${lang}.js`;
if (translationContext.keys().includes(path)) {
const module = translationContext(path);
return module.default;
} else {
return null;
}
}
/* stimulusFetch: 'lazy' */ /* stimulusFetch: 'lazy' */
export default class extends Controller { export default class extends Controller {
connect() { connect() {
@ -73,22 +51,8 @@ export default class extends Controller {
const language = document.body.dataset.locale ?? "en"; const language = document.body.dataset.locale ?? "en";
const emojiURL = new URL('../../ckeditor/emojis.json', import.meta.url).href;
const config = { const config = {
language: language, language: language,
licenseKey: "GPL",
emoji: {
definitionsUrl: emojiURL
}
}
//Load translations if not english
let translations = loadTranslation(language);
if (translations) {
//Keep existing translations (e.g. from other plugins), if any
config.translations = [window.CKEDITOR_TRANSLATIONS, translations];
} }
const watchdog = new EditorWatchdog(); const watchdog = new EditorWatchdog();
@ -106,18 +70,7 @@ export default class extends Controller {
editor_div.classList.add(...new_classes.split(",")); editor_div.classList.add(...new_classes.split(","));
} }
// Automatic synchronization of source input console.log(editor);
editor.model.document.on("change:data", () => {
editor.updateSourceElement();
// Dispatch the input event for further treatment
const event = new Event("input");
this.element.dispatchEvent(event);
});
//This return is important! Otherwise we get mysterious errors in the console
//See: https://github.com/ckeditor/ckeditor5/issues/5897#issuecomment-628471302
return editor;
}) })
.catch(error => { .catch(error => {
console.error(error); console.error(error);

View file

@ -27,7 +27,6 @@ export default class extends Controller {
deleteMessage: String, deleteMessage: String,
prototype: String, prototype: String,
rowsToDelete: Number, //How many rows (including the current one) shall be deleted after the current row rowsToDelete: Number, //How many rows (including the current one) shall be deleted after the current row
fieldPlaceholder: String
} }
static targets = ["target"]; static targets = ["target"];
@ -61,63 +60,24 @@ 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;
} }
const regexString = this.fieldPlaceholderValue || "__name__";
const regex = new RegExp(regexString, "g");
//Apply the index to prototype to create our element to insert //Apply the index to prototype to create our element to insert
const newElementStr = this.htmlDecode(prototype.replace(regex, this.generateUID())); const newElementStr = this.htmlDecode(prototype.replace(/__name__/g, this.generateUID()));
//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

View file

@ -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,22 +65,8 @@ 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()); return JSON.parse( localStorage.getItem(this.getStateSaveKey()) );
if(json === null || json === undefined) {
return null;
}
const data = JSON.parse(json);
if (data) {
//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;
//Reset the data length to the default value by deleting the length property
delete data.length;
}
return data;
} }
connect() { connect() {
@ -102,29 +81,17 @@ 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,
fixedHeader: { fixedHeader: {
header: $(window).width() >= 768, //Only enable fixedHeaders on devices with big screen. Fixes scrolling issues on smartphones. header: $(window).width() >= 768, //Only enable fixedHeaders on devices with big screen. Fixes scrolling issues on smartphones.
headerOffset: $("#navbar").outerHeight() headerOffset: $("#navbar").height()
}, },
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 +106,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 +117,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});
@ -188,11 +133,6 @@ export default class extends Controller {
dt.on('select.dt deselect.dt', this._onSelectionChange.bind(this)); dt.on('select.dt deselect.dt', this._onSelectionChange.bind(this));
}); });
promise.then((dt) => {
//Recalculate the fixed header offset, as the navbar should be rendered now
dt.fixedHeader.headerOffset($("#navbar").outerHeight());
});
//Allow to further configure the datatable //Allow to further configure the datatable
promise.then(this._afterLoaded.bind(this)); promise.then(this._afterLoaded.bind(this));
@ -200,12 +140,6 @@ export default class extends Controller {
console.debug('Datatables inited.'); console.debug('Datatables inited.');
} }
disconnect() {
//Destroy the datatable element
this._dt.destroy();
console.debug("Datatables destroyed.");
}
_rowCallback(row, data, index) { _rowCallback(row, data, index) {
//Set the row class based on the optional $$rowClass column data, can be used to color the rows //Set the row class based on the optional $$rowClass column data, can be used to color the rows
@ -231,16 +165,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();
}
} }

View file

@ -45,10 +45,8 @@ export default class extends DatatablesController {
//Hide/Unhide panel with the selection tools //Hide/Unhide panel with the selection tools
if (count > 0) { if (count > 0) {
selectPanel.classList.remove('d-none'); selectPanel.classList.remove('d-none');
selectPanel.classList.add('sticky-select-bar');
} else { } else {
selectPanel.classList.add('d-none'); selectPanel.classList.add('d-none');
selectPanel.classList.remove('sticky-select-bar');
} }
//Update selection count text //Update selection count text
@ -109,13 +107,6 @@ export default class extends DatatablesController {
//Hide the select element (the tomselect button is the sibling of the select element) //Hide the select element (the tomselect button is the sibling of the select element)
select_target.nextElementSibling.classList.add('d-none'); select_target.nextElementSibling.classList.add('d-none');
} }
//If the selected option has a data-turbo attribute, set it to the form
if (selected_option.dataset.turbo) {
this.element.dataset.turbo = selected_option.dataset.turbo;
} else {
this.element.dataset.turbo = true;
}
} }
confirmDeletionAtSubmit(event) { confirmDeletionAtSubmit(event) {

View file

@ -29,6 +29,33 @@ export default class extends Controller
this._confirmed = false; this._confirmed = false;
} }
click(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();
const message = this.element.dataset.deleteMessage;
const title = this.element.dataset.deleteTitle;
const that = this;
const confirm = bootbox.confirm({
message: message, title: title, callback: function (result) {
//If the dialog was confirmed, then submit the form.
if (result) {
that._confirmed = true;
event.target.click();
} else {
that._confirmed = false;
}
}
});
}
submit(event) { submit(event) {
//If a user has not already confirmed the deletion, just let turbo do its work //If a user has not already confirmed the deletion, just let turbo do its work
if(this._confirmed) { if(this._confirmed) {
@ -38,38 +65,26 @@ export default class extends Controller
//Prevent turbo from doing its work //Prevent turbo from doing its work
event.preventDefault(); event.preventDefault();
event.stopPropagation();
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;
//Create a clone of the event with the same submitter, so we can redispatch it if needed
//We need to do this that way, as we need the submitter info, just calling form.submit() would not work
this._our_event = new SubmitEvent('submit', {
submitter: event.submitter,
bubbles: true, //This line is important, otherwise Turbo will not receive the event
});
const confirm = bootbox.confirm({ const confirm = bootbox.confirm({
message: message, title: title, callback: function (result) { message: message, title: title, callback: function (result) {
//If the dialog was confirmed, then submit the form. //If the dialog was confirmed, then submit the form.
if (result) { if (result) {
//Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form
that._confirmed = true; that._confirmed = true;
form.dispatchEvent(that._our_event);
//Create a submit button in the form and click it to submit the form
//Before a submit event was dispatched, but this caused weird issues on Firefox causing the delete request being posted twice (and the second time was returning 404). See https://github.com/Part-DB/Part-DB-server/issues/273
const submit_btn = document.createElement('button');
submit_btn.type = 'submit';
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);
submit_btn.click();
} else { } else {
that._confirmed = false; that._confirmed = false;
} }

View file

@ -1,250 +0,0 @@
import { Controller } from "@hotwired/stimulus";
import "../../css/components/autocomplete_bootstrap_theme.css";
export default class extends Controller {
static targets = ["input"];
static values = {
partId: Number,
partCategoryId: Number,
partDescription: String,
suggestions: Object,
commonSectionHeader: String, // Dynamic header for common Prefixes
partIncrementHeader: String, // Dynamic header for new possible part increment
suggestUrl: String,
};
connect() {
this.configureAutocomplete();
this.watchCategoryChanges();
this.watchDescriptionChanges();
}
templates = {
commonSectionHeader({ title, html }) {
return html`
<section class="aa-Source">
<div class="aa-SourceHeader">
<span class="aa-SourceHeaderTitle">${title}</span>
<div class="aa-SourceHeaderLine"></div>
</div>
</section>
`;
},
partIncrementHeader({ title, html }) {
return html`
<section class="aa-Source">
<div class="aa-SourceHeader">
<span class="aa-SourceHeaderTitle">${title}</span>
<div class="aa-SourceHeaderLine"></div>
</div>
</section>
`;
},
list({ html }) {
return html`
<ul class="aa-List" role="listbox"></ul>
`;
},
item({ suggestion, description, html }) {
return html`
<li class="aa-Item" role="option" data-suggestion="${suggestion}" aria-selected="false">
<div class="aa-ItemWrapper">
<div class="aa-ItemContent">
<div class="aa-ItemIcon aa-ItemIcon--noBorder">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21c4.971 0 9-4.029 9-9s-4.029-9-9-9-9 4.029-9 9 4.029 9 9 9z"></path>
</svg>
</div>
<div class="aa-ItemContentBody">
<div class="aa-ItemContentTitle">${suggestion}</div>
<div class="aa-ItemContentDescription">${description}</div>
</div>
</div>
</div>
</li>
`;
},
};
configureAutocomplete() {
const inputField = this.inputTarget;
const commonPrefixes = this.suggestionsValue.commonPrefixes || [];
const prefixesPartIncrement = this.suggestionsValue.prefixesPartIncrement || [];
const commonHeader = this.commonSectionHeaderValue;
const partIncrementHeader = this.partIncrementHeaderValue;
if (!inputField || (!commonPrefixes.length && !prefixesPartIncrement.length)) return;
// Check whether the panel should be created at the update
if (this.isPanelInitialized) {
const existingPanel = inputField.parentNode.querySelector(".aa-Panel");
if (existingPanel) {
// Only remove the panel in the update phase
existingPanel.remove();
}
}
// Create panel
const panel = document.createElement("div");
panel.classList.add("aa-Panel");
panel.style.display = "none";
// Create panel layout
const panelLayout = document.createElement("div");
panelLayout.classList.add("aa-PanelLayout", "aa-Panel--scrollable");
// Section for prefixes part increment
if (prefixesPartIncrement.length) {
const partIncrementSection = document.createElement("section");
partIncrementSection.classList.add("aa-Source");
const partIncrementHeaderHtml = this.templates.partIncrementHeader({
title: partIncrementHeader,
html: String.raw,
});
partIncrementSection.innerHTML += partIncrementHeaderHtml;
const partIncrementList = document.createElement("ul");
partIncrementList.classList.add("aa-List");
partIncrementList.setAttribute("role", "listbox");
prefixesPartIncrement.forEach((prefix) => {
const itemHTML = this.templates.item({
suggestion: prefix.title,
description: prefix.description,
html: String.raw,
});
partIncrementList.innerHTML += itemHTML;
});
partIncrementSection.appendChild(partIncrementList);
panelLayout.appendChild(partIncrementSection);
}
// Section for common prefixes
if (commonPrefixes.length) {
const commonSection = document.createElement("section");
commonSection.classList.add("aa-Source");
const commonSectionHeader = this.templates.commonSectionHeader({
title: commonHeader,
html: String.raw,
});
commonSection.innerHTML += commonSectionHeader;
const commonList = document.createElement("ul");
commonList.classList.add("aa-List");
commonList.setAttribute("role", "listbox");
commonPrefixes.forEach((prefix) => {
const itemHTML = this.templates.item({
suggestion: prefix.title,
description: prefix.description,
html: String.raw,
});
commonList.innerHTML += itemHTML;
});
commonSection.appendChild(commonList);
panelLayout.appendChild(commonSection);
}
panel.appendChild(panelLayout);
inputField.parentNode.appendChild(panel);
inputField.addEventListener("focus", () => {
panel.style.display = "block";
});
inputField.addEventListener("blur", () => {
setTimeout(() => {
panel.style.display = "none";
}, 100);
});
// Selection of an item
panelLayout.addEventListener("mousedown", (event) => {
const target = event.target.closest("li");
if (target) {
inputField.value = target.dataset.suggestion;
panel.style.display = "none";
}
});
this.isPanelInitialized = true;
};
watchCategoryChanges() {
const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]');
const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]');
this.previousCategoryId = Number(this.partCategoryIdValue);
if (categoryField) {
categoryField.addEventListener("change", () => {
const categoryId = Number(categoryField.value);
const description = String(descriptionField?.value ?? '');
// Check whether the category has changed compared to the previous ID
if (categoryId !== this.previousCategoryId) {
this.fetchNewSuggestions(categoryId, description);
this.previousCategoryId = categoryId;
}
});
}
}
watchDescriptionChanges() {
const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]');
const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]');
this.previousDescription = String(this.partDescriptionValue);
if (descriptionField) {
descriptionField.addEventListener("input", () => {
const categoryId = Number(categoryField.value);
const description = String(descriptionField?.value ?? '');
// Check whether the description has changed compared to the previous one
if (description !== this.previousDescription) {
this.fetchNewSuggestions(categoryId, description);
this.previousDescription = description;
}
});
}
}
fetchNewSuggestions(categoryId, description) {
const baseUrl = this.suggestUrlValue;
const partId = this.partIdValue;
const truncatedDescription = description.length > 150 ? description.substring(0, 150) : description;
const encodedDescription = this.base64EncodeUtf8(truncatedDescription);
const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}` + (description !== '' ? `&description=${encodedDescription}` : '');
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
})
.then((response) => {
if (!response.ok) {
throw new Error(`Error when calling up the IPN-suggestions: ${response.status}`);
}
return response.json();
})
.then((data) => {
this.suggestionsValue = data;
this.configureAutocomplete();
})
.catch((error) => {
console.error("Errors when loading the new IPN-suggestions:", error);
});
};
base64EncodeUtf8(text) {
const utf8Bytes = new TextEncoder().encode(text);
return btoa(String.fromCharCode(...utf8Bytes));
};
}

View file

@ -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());
}
}

View file

@ -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;
}
}
});
}
}

View file

@ -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;
}
}

View file

@ -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);
});
}
}
}

View file

@ -10,19 +10,12 @@ export default class extends Controller {
connect() { connect() {
//Check if tomselect is inside an modal and do not attach the dropdown to body in that case (as it breaks the modal)
let dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
let settings = { let settings = {
allowEmptyOption: true, allowEmptyOption: true,
plugins: ['dropdown_input'], plugins: ['dropdown_input'],
searchField: ["name", "description", "category", "footprint"], searchField: ["name", "description", "category", "footprint"],
valueField: "id", valueField: "id",
labelField: "name", labelField: "name",
dropdownParent: dropdownParent,
preload: "focus", preload: "focus",
render: { render: {
item: (data, escape) => { item: (data, escape) => {
@ -34,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'>" +
@ -72,10 +65,4 @@ export default class extends Controller {
//this._tomSelect.clearOptions(); //this._tomSelect.clearOptions();
} }
} }
disconnect() {
super.disconnect();
//Destroy the TomSelect instance
this._tomSelect.destroy();
}
} }

View file

@ -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(" "));
}
}

View file

@ -38,17 +38,11 @@ export default class extends Controller {
this._emptyMessage = this.element.getAttribute('title'); this._emptyMessage = this.element.getAttribute('title');
} }
let dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
let settings = { let settings = {
plugins: ["clear_button"],
allowEmptyOption: true, allowEmptyOption: true,
selectOnTab: true, selectOnTab: true,
maxOptions: null, maxOptions: null,
dropdownParent: dropdownParent,
render: { render: {
item: this.renderItem.bind(this), item: this.renderItem.bind(this),
@ -56,24 +50,7 @@ export default class extends Controller {
} }
}; };
//Load the drag_drop plugin if the select is ordered
if (this.element.dataset.orderedValue) {
settings.plugins.push('drag_drop');
settings.plugins.push("caret_position");
}
//If multiple items can be selected, enable the remove_button plugin
if (this.element.multiple) {
settings.plugins.push('remove_button');
}
this._tomSelect = new TomSelect(this.element, settings); this._tomSelect = new TomSelect(this.element, settings);
//If the select is ordered, we need to update the value field (with the decoded value from the orderedValue field)
if (this.element.dataset.orderedValue) {
const data = JSON.parse(this.element.dataset.orderedValue);
this._tomSelect.setValue(data);
}
} }
getTomSelect() { getTomSelect() {
@ -107,10 +84,4 @@ export default class extends Controller {
return '<div>' + escape(data.text) + '</div>'; return '<div>' + escape(data.text) + '</div>';
} }
disconnect() {
super.disconnect();
//Destroy the TomSelect instance
this._tomSelect.destroy();
}
} }

View file

@ -20,29 +20,15 @@
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import TomSelect from "tom-select"; import TomSelect from "tom-select";
// TODO: Merge with select_controller.js
export default class extends Controller { export default class extends Controller {
_tomSelect; _tomSelect;
connect() { connect() {
let dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
this._tomSelect = new TomSelect(this.element, { this._tomSelect = new TomSelect(this.element, {
maxItems: 1000, maxItems: 1000,
allowEmptyOption: true, allowEmptyOption: true,
dropdownParent: dropdownParent,
plugins: ['remove_button'], plugins: ['remove_button'],
}); });
} }
disconnect() {
super.disconnect();
//Destroy the TomSelect instance
this._tomSelect.destroy();
}
} }

View file

@ -1,112 +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 dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
let settings = {
persistent: false,
create: true,
maxItems: 1,
maxOptions: 100,
createOnBlur: true,
selectOnTab: true,
valueField: 'text',
searchField: 'text',
orderField: 'text',
dropdownParent: dropdownParent,
//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();
}
}

View file

@ -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,24 +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 dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
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,
dropdownParent: dropdownParent,
searchField: [ searchField: [
{field: "text", weight : 2}, {field: "text", weight : 2},
@ -68,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>&nbsp;<strong>' + escape(data.input) + '</strong>&hellip;&nbsp;' + return '<div class="create"><i class="fa-solid fa-plus fa-fw"></i>&nbsp;<strong>' + escape(data.input) + '</strong>&hellip;&nbsp;' +
'<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() {
@ -187,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) + "&nbsp;→&nbsp;"; name += escape(data.parent) + "&nbsp;→&nbsp;";
} }
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>';
} }
@ -249,10 +127,4 @@ export default class extends Controller {
return '<div>' + level_html + escape(data.text) + image + symbol_badge + parent_badge + filter_badge + '</div>'; return '<div>' + level_html + escape(data.text) + image + symbol_badge + parent_badge + filter_badge + '</div>';
} }
disconnect() {
super.disconnect();
//Destroy the TomSelect instance
this._tomSelect.destroy();
}
} }

View file

@ -23,32 +23,18 @@ 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 dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
let settings = { let settings = {
plugins: { plugins: {
remove_button:{}, remove_button:{
'autoselect_typed': {}, }
'click_to_edit': {},
}, },
persistent: false, persistent: false,
selectOnTab: true,
createOnBlur: true, createOnBlur: true,
create: true, create: true,
dropdownParent: dropdownParent,
}; };
if(this.element.dataset.autocomplete) { if(this.element.dataset.autocomplete) {
@ -73,10 +59,4 @@ export default class extends Controller {
this._tomSelect = new TomSelect(this.element, settings); this._tomSelect = new TomSelect(this.element, settings);
} }
disconnect() {
super.disconnect();
//Destroy the TomSelect instance
this._tomSelect.destroy();
}
} }

View file

@ -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

View file

@ -1,136 +0,0 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["tbody", "addButton", "submitButton"]
static values = {
mappingIndex: Number,
maxMappings: Number,
prototype: String,
maxMappingsReachedMessage: String
}
connect() {
this.updateAddButtonState()
this.updateFieldOptions()
this.attachEventListeners()
}
attachEventListeners() {
// Add event listeners to existing field selects
const fieldSelects = this.tbodyTarget.querySelectorAll('select[name*="[field]"]')
fieldSelects.forEach(select => {
select.addEventListener('change', this.updateFieldOptions.bind(this))
})
// Note: Add button click is handled by Stimulus action in template (data-action="click->field-mapping#addMapping")
// No manual event listener needed
// Form submit handler
const form = this.element.querySelector('form')
if (form && this.hasSubmitButtonTarget) {
form.addEventListener('submit', this.handleFormSubmit.bind(this))
}
}
addMapping() {
const currentMappings = this.tbodyTarget.querySelectorAll('.mapping-row').length
if (currentMappings >= this.maxMappingsValue) {
alert(this.maxMappingsReachedMessageValue)
return
}
const newRowHtml = this.prototypeValue.replace(/__name__/g, this.mappingIndexValue)
const tempDiv = document.createElement('div')
tempDiv.innerHTML = newRowHtml
const fieldWidget = tempDiv.querySelector('select[name*="[field]"]') || tempDiv.children[0]
const providerWidget = tempDiv.querySelector('select[name*="[providers]"]') || tempDiv.children[1]
const priorityWidget = tempDiv.querySelector('input[name*="[priority]"]') || tempDiv.children[2]
const newRow = document.createElement('tr')
newRow.className = 'mapping-row'
newRow.innerHTML = `
<td>${fieldWidget ? fieldWidget.outerHTML : ''}</td>
<td>${providerWidget ? providerWidget.outerHTML : ''}</td>
<td>${priorityWidget ? priorityWidget.outerHTML : ''}</td>
<td>
<button type="button" class="btn btn-danger btn-sm" data-action="click->field-mapping#removeMapping">
<i class="fas fa-trash"></i>
</button>
</td>
`
this.tbodyTarget.appendChild(newRow)
this.mappingIndexValue++
const newFieldSelect = newRow.querySelector('select[name*="[field]"]')
if (newFieldSelect) {
newFieldSelect.value = ''
newFieldSelect.addEventListener('change', this.updateFieldOptions.bind(this))
}
this.updateFieldOptions()
this.updateAddButtonState()
}
removeMapping(event) {
const row = event.target.closest('tr')
row.remove()
this.updateFieldOptions()
this.updateAddButtonState()
}
updateFieldOptions() {
const fieldSelects = this.tbodyTarget.querySelectorAll('select[name*="[field]"]')
const selectedFields = Array.from(fieldSelects)
.map(select => select.value)
.filter(value => value && value !== '')
fieldSelects.forEach(select => {
Array.from(select.options).forEach(option => {
const isCurrentValue = option.value === select.value
const isEmptyOption = !option.value || option.value === ''
const isAlreadySelected = selectedFields.includes(option.value)
if (!isEmptyOption && isAlreadySelected && !isCurrentValue) {
option.disabled = true
option.style.display = 'none'
} else {
option.disabled = false
option.style.display = ''
}
})
})
}
updateAddButtonState() {
const currentMappings = this.tbodyTarget.querySelectorAll('.mapping-row').length
if (this.hasAddButtonTarget) {
if (currentMappings >= this.maxMappingsValue) {
this.addButtonTarget.disabled = true
this.addButtonTarget.title = this.maxMappingsReachedMessageValue
} else {
this.addButtonTarget.disabled = false
this.addButtonTarget.title = ''
}
}
}
handleFormSubmit(event) {
if (this.hasSubmitButtonTarget) {
this.submitButtonTarget.disabled = true
// Disable the entire form to prevent changes during processing
const form = event.target
const formElements = form.querySelectorAll('input, select, textarea, button')
formElements.forEach(element => {
if (element !== this.submitButtonTarget) {
element.disabled = true
}
})
}
}
}

View file

@ -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');
}
}
}

View file

@ -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;

View file

@ -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');
}
});
}
}

View file

@ -25,23 +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) {
//Escape percentage signs
value = this.inputTarget.value.replace(/%/g, '\\%');
value = "\\mathrm{" + value + "}";
} else {
value = this.inputTarget.value;
}
katex.render(value, this.previewTarget, {
throwOnError: false, throwOnError: false,
}); });
} }

View file

@ -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
{ {
@ -38,8 +31,6 @@ export default class extends Controller
static targets = ["name", "symbol", "unit"] static targets = ["name", "symbol", "unit"]
_tomSelect;
onItemAdd(value, item) { onItemAdd(value, item) {
//Retrieve the unit and symbol from the item //Retrieve the unit and symbol from the item
const symbol = item.dataset.symbol; const symbol = item.dataset.symbol;
@ -60,17 +51,13 @@ 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,
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin //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', delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
createOnBlur: true, createOnBlur: true,
selectOnTab: true,
create: true, create: true,
searchField: "name", searchField: "name",
//labelField: "name", //labelField: "name",
@ -85,9 +72,7 @@ export default class extends Controller
tmp += '<span>' + katex.renderToString(data.symbol) + '</span>' tmp += '<span>' + katex.renderToString(data.symbol) + '</span>'
} }
if (data.unit) { if (data.unit) {
let unit = data.unit.replace(/%/g, '\\%'); tmp += '<span class="ms-2">' + katex.renderToString('[' + data.unit + ']') + '</span>'
unit = "\\mathrm{" + unit + "}";
tmp += '<span class="ms-2">' + katex.renderToString('[' + unit + ']') + '</span>'
} }
@ -130,10 +115,4 @@ export default class extends Controller
this._tomSelect = new TomSelect(this.nameTarget, settings); this._tomSelect = new TomSelect(this.nameTarget, settings);
} }
disconnect() {
super.disconnect();
//Destroy the TomSelect instance
this._tomSelect.destroy();
}
} }

View file

@ -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');
}
}

View file

@ -1,68 +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';
export default class extends Controller {
static targets = ['items'];
static values = {
prototype: String,
prototypeName: { type: String, default: '__name__' },
index: { type: Number, default: 0 },
};
connect() {
if (!this.hasIndexValue || Number.isNaN(this.indexValue)) {
this.indexValue = this.itemsTarget?.children.length || 0;
}
}
add(event) {
event.preventDefault();
const encodedProto = this.prototypeValue || '';
const placeholder = this.prototypeNameValue || '__name__';
if (!encodedProto || !this.itemsTarget) return;
const protoHtml = this._decodeHtmlAttribute(encodedProto);
const idx = this.indexValue;
const html = protoHtml.replace(new RegExp(placeholder, 'g'), String(idx));
const wrapper = document.createElement('div');
wrapper.innerHTML = html;
const newItem = wrapper.firstElementChild;
if (newItem) {
this.itemsTarget.appendChild(newItem);
this.indexValue = idx + 1;
}
}
remove(event) {
event.preventDefault();
const row = event.currentTarget.closest('.tc-item');
if (row) row.remove();
}
_decodeHtmlAttribute(str) {
const tmp = document.createElement('textarea');
tmp.innerHTML = str;
return tmp.value || tmp.textContent || '';
}
}

View file

@ -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 - 2024 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,15 +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\Settings;
#[\Attribute(\Attribute::TARGET_CLASS)]
class SettingsIcon
{
public function __construct(public string $icon)
{ {
connect() {
this.element.onclick = function() {
window.u2fauth.register();
}
} }
} }

View file

@ -1,86 +0,0 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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 '../css/components/toggle_password.css';
export default class extends Controller {
static values = {
visibleLabel: { type: String, default: 'Show' },
visibleIcon: { type: String, default: 'Default' },
hiddenLabel: { type: String, default: 'Hide' },
hiddenIcon: { type: String, default: 'Default' },
buttonClasses: Array,
};
isDisplayed = false;
visibleIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="toggle-password-icon" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
</svg>`;
hiddenIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="toggle-password-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd" />
<path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z" />
</svg>`;
connect() {
if (this.visibleIconValue !== 'Default') {
this.visibleIcon = this.visibleIconValue;
}
if (this.hiddenIconValue !== 'Default') {
this.hiddenIcon = this.hiddenIconValue;
}
const button = this.createButton();
this.element.insertAdjacentElement('afterend', button);
this.dispatchEvent('connect', { element: this.element, button });
}
/**
* @returns {HTMLButtonElement}
*/
createButton() {
const button = document.createElement('button');
button.type = 'button';
button.classList.add(...this.buttonClassesValue);
button.setAttribute('tabindex', '-1');
button.addEventListener('click', this.toggle.bind(this));
button.innerHTML = `${this.visibleIcon} ${this.visibleLabelValue}`;
return button;
}
/**
* Toggle input type between "text" or "password" and update label accordingly
*/
toggle(event) {
this.isDisplayed = !this.isDisplayed;
const toggleButtonElement = event.currentTarget;
toggleButtonElement.innerHTML = this.isDisplayed
? `${this.hiddenIcon} ${this.hiddenLabelValue}`
: `${this.visibleIcon} ${this.visibleLabelValue}`;
this.element.setAttribute('type', this.isDisplayed ? 'text' : 'password');
this.dispatchEvent(this.isDisplayed ? 'show' : 'hide', { element: this.element, button: toggleButtonElement });
}
dispatchEvent(name, payload) {
this.dispatch(name, { detail: payload, prefix: 'toggle-password' });
}
}

View file

@ -99,32 +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;
}
/****************************************
* Password toggle
****************************************/
.toggle-password-button {
top: 0.7rem !important;
}

View file

@ -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 - 2025 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,15 +17,16 @@
* 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); .darkmode-layer {
z-index: 2020;
namespace App\Entity\InfoProviderSystem; }
/** If darkmode is enabled revert the blening for images and videos, as these should be shown not inverted */
enum BulkImportPartStatus: string .darkmode--activated img,
{ .darkmode--activated video {
case PENDING = 'pending'; mix-blend-mode: difference;
case COMPLETED = 'completed'; }
case SKIPPED = 'skipped';
case FAILED = 'failed'; .darkmode--activated .hoverpic:hover {
background: black;
} }

View file

@ -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;
}

View file

@ -18,8 +18,8 @@
*/ */
.hoverpic { .hoverpic {
min-width: var(--table-image-preview-min-size, 20px); min-width: 10px;
max-width: var(--table-image-preview-max-size, 35px); max-width: 30px;
display: block; display: block;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@ -47,17 +47,3 @@
max-width: 2.0em; max-width: 2.0em;
object-fit: contain; object-fit: contain;
} }
.part-table-image {
max-height: calc(1.2*var(--table-image-preview-max-size, 35px)); /** Aspect ratio of maximum 1.2 */
object-fit: contain;
}
.part-info-image {
max-height: 350px;
object-fit: contain;
}
.object-fit-cover {
object-fit: cover;
}

View file

@ -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;
} }
@ -133,7 +135,7 @@ showing the sidebar (on devices with md or higher)
*/ */
#sidebar-toggle-button { #sidebar-toggle-button {
position: fixed; position: fixed;
left: 2px; left: 3px;
bottom: 50%; bottom: 50%;
} }

View file

@ -17,16 +17,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/****************************************
* Action bar
****************************************/
.sticky-select-bar {
position: sticky;
top: 120px;
z-index: 1000; /* Ensure the bar is above other content */
}
/**************************************** /****************************************
* Tables * Tables
****************************************/ ****************************************/
@ -73,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;
} }
@ -90,29 +84,13 @@ th.select-checkbox {
* Datatables definitions/overrides * Datatables definitions/overrides
********************************************************************/ ********************************************************************/
.dt-length { .dataTables_length {
display: inline-flex; display: inline-flex;
} }
/** Add spacing between column visibility button and length menu */
.buttons-colvis {
margin-right: 0.2em !important;
}
/** 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, table.dataTable tr.selected th.select-checkbox:after {
{ margin-top: -28px !important;
margin-top: -20px !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);
} }

File diff suppressed because it is too large Load diff

View file

@ -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,44 +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);
--ck-content-font-color: var(--ck-color-base-text);
}

View file

@ -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;
}

View file

@ -1,41 +0,0 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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/>.
*/
.toggle-password-container {
position: relative;
}
.toggle-password-icon {
height: 1rem;
width: 1rem;
}
.toggle-password-button {
align-items: center;
background-color: transparent;
border: none;
column-gap: 0.25rem;
display: flex;
flex-direction: row;
font-size: 0.875rem;
justify-items: center;
height: 1rem;
line-height: 1.25rem;
position: absolute;
right: 0.5rem;
top: -1.25rem;
}

View file

@ -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

View file

@ -1,3 +0,0 @@
# Ignore font files
*.otf
*.ttf

View file

@ -1 +0,0 @@
Put your font ttf files in this folder to make them available to the label generator.

View file

@ -22,13 +22,14 @@
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';
import '../css/app/images.css'; import '../css/app/images.css';
// start the Stimulus application // start the Stimulus application
import '../stimulus_bootstrap'; import '../bootstrap';
// Need jQuery? Install it with "yarn add jquery", then uncomment to require it. // Need jQuery? Install it with "yarn add jquery", then uncomment to require it.
const $ = require('jquery'); const $ = require('jquery');
@ -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/ponyfill";
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;
},
});

View file

@ -27,67 +27,14 @@ class ErrorHandlerHelper {
constructor() { constructor() {
console.log('Error Handler registered'); console.log('Error Handler registered');
//const content = document.getElementById('content'); const content = document.getElementById('content');
//It seems that the content element is unreliable for these events, so we use the document instead content.addEventListener('turbo:before-fetch-response', (event) => this.handleError(event));
const content = document;
//content.addEventListener('turbo:before-fetch-response', (event) => this.handleError(event));
content.addEventListener('turbo:fetch-request-error', (event) => this.handleError(event));
content.addEventListener('turbo:frame-missing', (event) => this.handleError(event));
$(document).ajaxError(this.handleJqueryErrror.bind(this)); $(document).ajaxError(this.handleJqueryErrror.bind(this));
} }
_showAlert(statusText, statusCode, location, responseHTML) _showAlert(statusText, statusCode, location, responseHTML)
{ {
const httpStatusToText = {
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'306': 'Unused',
'307': 'Temporary Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Required',
'413': 'Request Entry Too Large',
'414': 'Request-URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Requested Range Not Satisfiable',
'417': 'Expectation Failed',
'418': 'I\'m a teapot',
'429': 'Too Many Requests',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
};
//If the statusText is empty, we use the status code as text
if (!statusText) {
statusText = httpStatusToText[statusCode];
}
//Create error text //Create error text
const title = statusText + ' (Status ' + statusCode + ')'; const title = statusText + ' (Status ' + statusCode + ')';
@ -140,10 +87,8 @@ class ErrorHandlerHelper {
} }
handleError(event) { handleError(event) {
//Prevent default error handling const fetchResponse = event.detail.fetchResponse;
event.preventDefault(); const response = fetchResponse.response;
const response = event.detail.response;
//Ignore aborted requests. //Ignore aborted requests.
if (response.statusText === 'abort' || response.status == 0) { if (response.statusText === 'abort' || response.status == 0) {
@ -155,17 +100,11 @@ class ErrorHandlerHelper {
return; return;
} }
//Skip 404 errors, on admin pages (as this causes a popup on deletion in firefox) if(fetchResponse.failed) {
if (response.status == 404 && event.target.id === 'admin-content-frame') {
return;
}
if(!response.ok) {
response.text().then(responseHTML => { response.text().then(responseHTML => {
this._showAlert(response.statusText, response.status, response.url, responseHTML); this._showAlert(response.statusText, response.status, fetchResponse.location.toString(), responseHTML);
}).catch(err => { }).catch(err => {
this._showAlert(response.statusText, response.status, response.url, '<pre>' + err + '</pre>'); this._showAlert(response.statusText, response.status, fetchResponse.location.toString(), '<pre>' + err + '</pre>');
}); });
} }
} }

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more